├── .all-contributorsrc
├── .gitattributes
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── bsconfig.json
├── commitlint.config.js
├── docs
├── ReasonApolloHooks
│ ├── ApolloHooks
│ │ └── index.html
│ ├── ApolloHooksMutation
│ │ └── index.html
│ ├── ApolloHooksProvider
│ │ └── index.html
│ ├── ApolloHooksQuery
│ │ └── index.html
│ ├── ApolloHooksSubscription
│ │ └── index.html
│ ├── ApolloHooksTypes
│ │ └── index.html
│ └── index.html
├── highlight.pack.js
├── index.html
└── odoc.css
├── examples
└── persons
│ ├── .gitignore
│ ├── README.md
│ ├── bsconfig.json
│ ├── build
│ ├── Index.js
│ └── index.html
│ ├── graphql_schema.json
│ ├── package.json
│ ├── src
│ ├── AddPerson.re
│ ├── Client.re
│ ├── EditPerson.re
│ ├── FilterByAge.re
│ ├── FilterByAgeErrorHandling.re
│ ├── FilterByNameCache.re
│ ├── Index.re
│ ├── LoadMore.re
│ ├── Persons.re
│ ├── Root.re
│ ├── SubscribeToMore.re
│ ├── fragments
│ │ ├── FilterByAgeFragment.re
│ │ ├── Fragments.re
│ │ └── LoadMoreFragments.re
│ ├── index.html
│ └── styles.css
│ ├── webpack.config.js
│ └── yarn.lock
├── package.json
├── src
├── ApolloHooks.re
├── ApolloHooksMutation.re
├── ApolloHooksProvider.re
├── ApolloHooksQuery.re
├── ApolloHooksSubscription.re
├── ApolloHooksTypes.re
└── index.mld
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "fakenickels",
10 | "name": "Gabriel Rubens",
11 | "avatar_url": "https://avatars0.githubusercontent.com/u/1283200?v=4",
12 | "profile": "http://twitter.com/fakenickels",
13 | "contributions": [
14 | "code",
15 | "doc",
16 | "ideas"
17 | ]
18 | },
19 | {
20 | "login": "arielschiavoni",
21 | "name": "Ariel Schiavoni",
22 | "avatar_url": "https://avatars2.githubusercontent.com/u/1364564?v=4",
23 | "profile": "https://github.com/arielschiavoni",
24 | "contributions": [
25 | "doc"
26 | ]
27 | },
28 | {
29 | "login": "hew",
30 | "name": "Matt",
31 | "avatar_url": "https://avatars0.githubusercontent.com/u/3103241?v=4",
32 | "profile": "https://playqup.com",
33 | "contributions": [
34 | "code"
35 | ]
36 | },
37 | {
38 | "login": "baransu",
39 | "name": "Tomasz Cichocinski",
40 | "avatar_url": "https://avatars2.githubusercontent.com/u/9558691?v=4",
41 | "profile": "https://twitter.com/_cichocinski",
42 | "contributions": [
43 | "bug",
44 | "code"
45 | ]
46 | },
47 | {
48 | "login": "tmattio",
49 | "name": "Thibaut Mattio",
50 | "avatar_url": "https://avatars0.githubusercontent.com/u/6162008?v=4",
51 | "profile": "https://tmattio.github.io/",
52 | "contributions": [
53 | "code"
54 | ]
55 | },
56 | {
57 | "login": "Emilios1995",
58 | "name": "Emilio Srougo",
59 | "avatar_url": "https://avatars1.githubusercontent.com/u/12430923?v=4",
60 | "profile": "https://github.com/Emilios1995",
61 | "contributions": [
62 | "bug"
63 | ]
64 | },
65 | {
66 | "login": "athaeryn",
67 | "name": "Mike Anderson",
68 | "avatar_url": "https://avatars0.githubusercontent.com/u/1226972?v=4",
69 | "profile": "http://mkndrsn.com",
70 | "contributions": [
71 | "code"
72 | ]
73 | },
74 | {
75 | "login": "yurijean",
76 | "name": "Yuri Jean Fabris",
77 | "avatar_url": "https://avatars0.githubusercontent.com/u/6414876?v=4",
78 | "profile": "https://github.com/yurijean",
79 | "contributions": [
80 | "code"
81 | ]
82 | },
83 | {
84 | "login": "MargaretKrutikova",
85 | "name": "Margarita Krutikova",
86 | "avatar_url": "https://avatars2.githubusercontent.com/u/5932274?v=4",
87 | "profile": "https://twitter.com/rita_krutikova",
88 | "contributions": [
89 | "code",
90 | "review",
91 | "ideas"
92 | ]
93 | },
94 | {
95 | "login": "Yakimych",
96 | "name": "Kyrylo Yakymenko",
97 | "avatar_url": "https://avatars1.githubusercontent.com/u/5010901?v=4",
98 | "profile": "https://github.com/Yakimych",
99 | "contributions": [
100 | "bug",
101 | "code"
102 | ]
103 | },
104 | {
105 | "login": "lukashambsch",
106 | "name": "Lukas Hambsch",
107 | "avatar_url": "https://avatars3.githubusercontent.com/u/7560008?v=4",
108 | "profile": "https://github.com/lukashambsch",
109 | "contributions": [
110 | "bug"
111 | ]
112 | },
113 | {
114 | "login": "jfrolich",
115 | "name": "Jaap Frolich",
116 | "avatar_url": "https://avatars1.githubusercontent.com/u/579279?v=4",
117 | "profile": "http://www.familyfive.app",
118 | "contributions": [
119 | "code",
120 | "review",
121 | "ideas"
122 | ]
123 | },
124 | {
125 | "login": "believer",
126 | "name": "Rickard Laurin",
127 | "avatar_url": "https://avatars1.githubusercontent.com/u/1478102?v=4",
128 | "profile": "https://willcodefor.beer/",
129 | "contributions": [
130 | "bug"
131 | ]
132 | },
133 | {
134 | "login": "medson10",
135 | "name": "Medson Oliveira",
136 | "avatar_url": "https://avatars0.githubusercontent.com/u/17956325?v=4",
137 | "profile": "http://medson.me",
138 | "contributions": [
139 | "code",
140 | "review",
141 | "ideas"
142 | ]
143 | },
144 | {
145 | "login": "soulplant",
146 | "name": "soulplant",
147 | "avatar_url": "https://avatars3.githubusercontent.com/u/16846?v=4",
148 | "profile": "https://github.com/soulplant",
149 | "contributions": [
150 | "code"
151 | ]
152 | },
153 | {
154 | "login": "mbirkegaard",
155 | "name": "mbirkegaard",
156 | "avatar_url": "https://avatars0.githubusercontent.com/u/18616185?v=4",
157 | "profile": "https://github.com/mbirkegaard",
158 | "contributions": [
159 | "code"
160 | ]
161 | },
162 | {
163 | "login": "strdr4605",
164 | "name": "Dragoș Străinu",
165 | "avatar_url": "https://avatars3.githubusercontent.com/u/16056918?v=4",
166 | "profile": "https://strdr4605.github.io",
167 | "contributions": [
168 | "doc"
169 | ]
170 | },
171 | {
172 | "login": "bdunn313",
173 | "name": "Brad Dunn",
174 | "avatar_url": "https://avatars3.githubusercontent.com/u/867683?v=4",
175 | "profile": "https://github.com/bdunn313",
176 | "contributions": [
177 | "doc"
178 | ]
179 | }
180 | ],
181 | "contributorsPerLine": 7,
182 | "projectName": "reason-apollo-hooks",
183 | "projectOwner": "Astrocoders",
184 | "repoType": "github",
185 | "repoHost": "https://github.com",
186 | "skipCi": true
187 | }
188 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Tell github that .re and .rei files are Reason
2 | *.re linguist-language=Reason
3 | *.rei linguist-language=Reason
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .merlin
3 | .bsb.lock
4 | lib/
5 | *.bs.js
6 | yarn-error.log
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .merlin
3 | .bsb.lock
4 | lib/
5 | examples/
6 | *.bs.js
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Astrocoders
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚠️ Prefer the new https://github.com/reasonml-community/reason-apollo-client instead
2 |
3 | ---
4 |
5 | # reason-apollo-hooks
6 |
7 |
8 |
9 | [](#contributors-)
10 |
11 |
12 |
13 | Reason bindings for the official [@apollo/react-hooks](https://www.npmjs.com/package/@apollo/react-hooks)
14 |
15 | ## Table of contents
16 |
17 | - [reason-apollo-hooks](#reason-apollo-hooks)
18 | - [Table of contents](#table-of-contents)
19 | - [Installation :arrow_up:](#installation-arrowup)
20 | - [Setting up :arrow_up:](#setting-up-arrowup)
21 | - [Usage with reason-apollo :arrow_up:](#usage-with-reason-apollo-arrowup)
22 | - [Available hooks :arrow_up:](#available-hooks-arrowup)
23 | - [useQuery :arrow_up:](#usequery-arrowup)
24 | - [useMutation :arrow_up:](#usemutation-arrowup)
25 | - [useSubscription :arrow_up:](#usesubscription-arrowup)
26 | - [Cache :arrow_up:](#cache-arrowup)
27 | - [Fragment :arrow_up:](#fragment-arrowup)
28 | - [Getting it running](#getting-it-running)
29 | - [Contributors ✨](#contributors-%e2%9c%a8)
30 |
31 | ## Installation [:arrow_up:](#table-of-contents)
32 |
33 | ```
34 | yarn add reason-apollo-hooks reason-apollo@0.19.0 @apollo/react-hooks
35 | ```
36 |
37 | BuckleScript <= 5.0.0
38 |
39 | ```
40 | yarn add reason-apollo-hooks@3.0.0 reason-apollo@0.17.0 @apollo/react-hooks
41 | ```
42 |
43 | Follow the installation instructions of [graphql_ppx_re](https://github.com/baransu/graphql_ppx_re).
44 |
45 | Then update your bsconfig.json
46 |
47 | ```diff
48 | "bs-dependencies": [
49 | ...
50 | + "reason-apollo-hooks",
51 | + "reason-apollo"
52 | ]
53 | ```
54 |
55 | ## Setting up [:arrow_up:](#table-of-contents)
56 |
57 | Add the provider in the top of the tree
58 |
59 | ```reason
60 | /* Create an InMemoryCache */
61 | let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();
62 |
63 | /* Create an HTTP Link */
64 | let httpLink =
65 | ApolloLinks.createHttpLink(~uri="http://localhost:3010/graphql", ());
66 |
67 | let client =
68 | ReasonApollo.createApolloClient(~link=httpLink, ~cache=inMemoryCache, ());
69 |
70 | let app =
71 |
72 | ...
73 |
74 | ```
75 |
76 | ### Usage with reason-apollo [:arrow_up:](#table-of-contents)
77 |
78 | To use with `reason-apollo`'s `ReasonApollo.Provider` already present in your project:
79 |
80 | ```reason
81 | let client = ... // create Apollo client
82 |
83 | ReactDOMRe.renderToElementWithId(
84 |
85 |
86 |
87 |
88 | ,
89 | "root",
90 | );
91 | ```
92 |
93 | ## Available hooks [:arrow_up:](#table-of-contents)
94 |
95 | ### useQuery [:arrow_up:](#table-of-contents)
96 |
97 | ```reason
98 | open ApolloHooks
99 |
100 | module UserQuery = [%graphql {|
101 | query UserQuery {
102 | currentUser {
103 | name
104 | }
105 | }
106 | |}];
107 |
108 | [@react.component]
109 | let make = () => {
110 | /* Both variant and records available */
111 | let (simple, _full) = useQuery(UserQuery.definition);
112 |
113 |
114 | {
115 | switch(simple) {
116 | | Loading =>
{React.string("Loading...")}
117 | | Data(data) =>
118 |
{React.string(data##currentUser##name)}
119 | | NoData
120 | | Error(_) =>
{React.string("Get off my lawn!")}
121 | }
122 | }
123 |
124 | }
125 | ```
126 |
127 | Using the `full` record for more advanced cases
128 |
129 | ```reason
130 | [@react.component]
131 | let make = () => {
132 | /* Both variant and records available */
133 | let (_simple, full) = useQuery(UserQuery.definition);
134 |
135 |
136 | {
137 | switch(full) {
138 | | { loading: true }=>
{React.string("Loading...")}
139 | | { data: Some(data) } =>
140 |
{React.string(data##currentUser##name)}
141 | | any other possibilities =>
142 | | { error: Some(_) } =>
{React.string("Get off my lawn!")}
143 | }
144 | }
145 |
146 | }
147 | ```
148 |
149 | Using `fetchPolicy` to change interactions with the `apollo` cache, see [apollo docs](https://www.apollographql.com/docs/react/api/react-apollo/#optionsfetchpolicy).
150 |
151 | ```reason
152 | let (_simple, full) = useQuery(~fetchPolicy=NetworkOnly, UserQuery.definition);
153 | ```
154 |
155 | Using `errorPolicy` to change how errors are handled, see [apollo docs](https://www.apollographql.com/docs/react/api/react-apollo/#optionserrorpolicy).
156 |
157 | ```reason
158 | let (simple, _full) = useQuery(~errorPolicy=All, UserQuery.definition);
159 | ```
160 |
161 | Using `skip` to skip query entirely, see [apollo docs](https://www.apollographql.com/docs/react/api/react-apollo/#configskip).
162 |
163 | ```reason
164 | let (simple, _full) =
165 | useQuery(
166 | ~skip=
167 | switch (value) {
168 | | None => true
169 | | _ => false
170 | },
171 | UserQuery.definition,
172 | );
173 | ```
174 |
175 | ### useMutation [:arrow_up:](#table-of-contents)
176 |
177 | ```reason
178 | module ScreamMutation = [%graphql {|
179 | mutation ScreamMutation($screamLevel: Int!) {
180 | scream(level: $screamLevel) {
181 | error
182 | }
183 | }
184 | |}];
185 |
186 | [@react.component]
187 | let make = () => {
188 | /* Both variant and records available */
189 | let ( screamMutation, simple, _full ) =
190 | useMutation(~variables=ScreamMutation.makeVariables(~screamLevel=10, ()), ScreamMutation.definition);
191 | let scream = (_) => {
192 | screamMutation()
193 | |> Js.Promise.then_(((simple, _full)) => {
194 | // Trigger side effects by chaining the promise returned by screamMutation()
195 | switch (simple) {
196 | // You *must* set the error policy to be able to handle errors
197 | // in then_. See EditPersons.re for more
198 | | ApolloHooks.Mutation.Errors(_theErrors) => Js.log("OH NO!")
199 | | NoData => Js.log("NO DATA?")
200 | | Data(_theData) => Js.log("DATA!")
201 | };
202 | Js.Promise.resolve();
203 | })
204 | |> ignore
205 | }
206 |
207 | // Use simple (and/or full) for (most) UI feedback
208 |
209 | {switch (simple) {
210 | | NotCalled
211 | | Data(_) => React.null
212 | | Loading =>
"Screaming!"->React.string
213 | | NoData
214 | | Error(_) =>
"Something went wrong!"->React.string
215 | }}
216 |
217 | {React.string("You kids get off my lawn!")}
218 |
219 |
220 | }
221 | ```
222 |
223 | If you don't know the value of the variables yet you can pass them in later
224 |
225 | ```reason
226 | [@react.component]
227 | let make = () => {
228 | /* Both variant and records available */
229 | let ( screamMutation, _simple, _full ) = useMutation(ScreamMutation.definition);
230 | let scream = (_) => {
231 | screamMutation(~variables=ScreamMutation.makeVariables(~screamLevel=10, ()), ())
232 | |> Js.Promise.then_(((simple, _full)) => {
233 | ...
234 | })
235 | |> ignore
236 | }
237 |
238 |
239 |
240 | {React.string("You kids get off my lawn!")}
241 |
242 |
243 | }
244 | ```
245 |
246 | ### useSubscription [:arrow_up:](#table-of-contents)
247 |
248 | In order to use subscriptions, you first need to set up your websocket link:
249 |
250 | ```diff
251 | /* Create an InMemoryCache */
252 | let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();
253 |
254 | /* Create an HTTP Link */
255 | let httpLink =
256 | ApolloLinks.createHttpLink(~uri="http://localhost:3010/graphql", ());
257 | +
258 | +/* Create a WS Link */
259 | +let webSocketLink =
260 | + ApolloLinks.webSocketLink({
261 | + uri: "wss://localhost:3010/graphql",
262 | + options: {
263 | + reconnect: true,
264 | + connectionParams: None,
265 | + },
266 | + });
267 | +
268 | +/* Using the ability to split links, you can send data to each link
269 | + depending on what kind of operation is being sent */
270 | +let link =
271 | + ApolloLinks.split(
272 | + operation => {
273 | + let operationDefition =
274 | + ApolloUtilities.getMainDefinition(operation.query);
275 | + operationDefition.kind == "OperationDefinition"
276 | + && operationDefition.operation == "subscription";
277 | + },
278 | + webSocketLink,
279 | + httpLink,
280 | + );
281 |
282 | let client =
283 | - ReasonApollo.createApolloClient(~link=httpLink, ~cache=inMemoryCache, ());
284 | + ReasonApollo.createApolloClient(~link, ~cache=inMemoryCache, ());
285 |
286 | let app =
287 |
288 | ...
289 |
290 | ```
291 |
292 | Then, you can implement `useSubscription` in a similar manner to `useQuery`
293 |
294 | ```reason
295 | module UserAdded = [%graphql {|
296 | subscription userAdded {
297 | userAdded {
298 | id
299 | name
300 | }
301 | }
302 | |}];
303 |
304 |
305 | [@react.component]
306 | let make = () => {
307 | let (userAddedSubscription, _full) = ApolloHooks.useSubscription(UserAdded.definition);
308 |
309 | switch (userAddedSubscription) {
310 | | Loading => {ReasonReact.string("Loading")}
311 | | Error(error) => {ReasonReact.string(error##message)}
312 | | Data(_response) =>
313 |
314 |
315 |
316 |
317 | };
318 | };
319 | ```
320 |
321 | ## Cache [:arrow_up:](#table-of-contents)
322 |
323 | There are a couple of caveats with manual cache updates.
324 |
325 | **TL;DR**
326 |
327 | 1. If you need to remove items from cached data, it is enough to just filter them out and save the result into cache as is.
328 | 2. If you need to add the result of a mutation to a list of items with the same shape, you simply concat it with the list and save into cache as it.
329 | 3. When you need to update a field, you have to resort to raw javascript to use spread operator on `Js.t` object in order to preserve `__typename` that `apollo` adds to all queries by default.
330 |
331 | An example of cache update could look like this:
332 |
333 | ```reason
334 | module PersonsQuery = [%graphql
335 | {|
336 | query getAllPersons {
337 | allPersons {
338 | id
339 | age
340 | name
341 | }
342 | }
343 | |}
344 | ];
345 |
346 | module PersonsReadQuery = ApolloClient.ReadQuery(PersonsQuery);
347 | module PersonsWriteQuery = ApolloClient.WriteQuery(PersonsQuery);
348 |
349 | external cast: Js.Json.t => PersonsQuery.t = "%identity";
350 |
351 | let updatePersons = (~client, ~name, ~age) => {
352 | let query = PersonsQuery.make();
353 | let readQueryOptions = ApolloHooks.Utils.toReadQueryOptions(query);
354 |
355 | // can throw exception of cache is empty
356 | switch (PersonsReadQuery.readQuery(client, readQueryOptions)) {
357 | | exception _ => ()
358 | | cachedResponse =>
359 | switch (cachedResponse |> Js.Nullable.toOption) {
360 | | None => ()
361 | | Some(cachedPersons) =>
362 | // readQuery will return unparsed data with __typename field, need to cast it since
363 | // it has type Json.t, but we know it will have the same type as PersonsReadQuery.t
364 | let persons = cast(cachedPersons);
365 |
366 | // to remove items, simply filter them out
367 | let updatedPersons = {
368 | "allPersons":
369 | Belt.Array.keep(persons##allPersons, person => person##age !== age),
370 | };
371 |
372 | // when updating items, __typename must be preserved, but since it is not a record,
373 | // can't use spread, so use JS to update items
374 | let updatedPersons = {
375 | "allPersons":
376 | Belt.Array.map(persons##allPersons, person =>
377 | person##name === name ? [%bs.raw {| {...person, age } |}] : person
378 | ),
379 | };
380 |
381 | PersonsWriteQuery.make(
382 | ~client,
383 | ~variables=query##variables,
384 | ~data=updatedPersons,
385 | (),
386 | );
387 | }
388 | };
389 | };
390 | ```
391 |
392 | `reason-apollo-hooks` parses response data from a query or mutation using parse function created by `graphql_ppx`. For example, when using `@bsRecord` directive, the response object will be parsed from a `Js.t` object to a reason record. In this case, the response data in reason code is not the same object that is stored in cache, since `react-apollo` saves data in cache before it is parsed and returned to the component. However, when updating cache, the data must be in the same format or apollo cache won't work correctly and throw errors.
393 |
394 | If using directives like `@bsRecord`, `@bsDecoder` or `@bsVariant` in `graphql_ppx`, the data needs to be serialized back to JS object before it is written in cache. Since there is currently no way to serialize this data (see [this issue](https://github.com/mhallin/graphql_ppx/issues/71) in `graphql_ppx`), queries that will be updated in cache shouldn't use any of those directive, unless you will take care of the serialization yourself.
395 |
396 | By default, apollo will add field `__typename` to the queries and will use it to normalize data and manipulate cache (see [normalization](https://www.apollographql.com/docs/react/advanced/caching/#normalization)). This field won't exist on parsed reason objects, since it is not included in the actual query you write, but is added by apollo before sending the query. Since `__typename` is crucial for the cache to work correctly, we need to read data from cache in its raw unparsed format, which is achieved with `readQuery` from `ApolloClient.ReadQuery` defined in `reason-apollo`.
397 |
398 |
399 | ## Fragment [:arrow_up:](#table-of-contents)
400 |
401 | Using [fragments](https://www.apollographql.com/docs/react/data/fragments/).
402 |
403 | Fragments can be defined and used like this:
404 |
405 | ```reason
406 | // Fragments.re
407 | module PersonFragment = [%graphql
408 | {|
409 | fragment person on Person {
410 | id
411 | name
412 | age
413 | }
414 | |}
415 | ];
416 |
417 | ```
418 |
419 | ```reason
420 | module PersonsQuery = [%graphql
421 | {|
422 | query getAllPersons {
423 | ...Fragments.PersonFragment.Person
424 | }
425 | |}
426 | ];
427 | ```
428 |
429 | See [examples/persons/src/fragments/LoadMoreFragments.re](examples/persons/src/fragments/LoadMoreFragments.re).
430 |
431 | ## Getting it running
432 |
433 | ```sh
434 | yarn
435 | yarn start
436 | ```
437 |
438 | ## Contributors ✨
439 |
440 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
441 |
442 |
443 |
444 |
445 |
471 |
472 |
473 |
474 |
475 |
476 |
477 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
478 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reason-apollo-hooks",
3 | "reason": {
4 | "react-jsx": 3
5 | },
6 | "sources": [
7 | {
8 | "dir": "src",
9 | "subdirs": true
10 | },
11 | {
12 | "dir": "examples",
13 | "type": "dev"
14 | }
15 | ],
16 | "package-specs": [
17 | {
18 | "module": "commonjs",
19 | "in-source": true
20 | }
21 | ],
22 | "suffix": ".bs.js",
23 | "bs-dependencies": ["reason-react", "reason-apollo"],
24 | "refmt": 3
25 | }
26 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'body-leading-blank': [1, 'always'],
4 | 'footer-leading-blank': [1, 'always'],
5 | 'header-max-length': [2, 'always', 72],
6 | 'scope-case': [2, 'always', 'lower-case'],
7 | 'subject-case': [
8 | 2,
9 | 'never',
10 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case']
11 | ],
12 | 'subject-empty': [2, 'never'],
13 | 'subject-full-stop': [2, 'never', '.'],
14 | 'type-case': [2, 'always', 'lower-case'],
15 | 'type-empty': [2, 'never'],
16 | 'type-enum': [
17 | 2,
18 | 'always',
19 | [
20 | 'build',
21 | 'chore',
22 | 'ci',
23 | 'docs',
24 | 'feat',
25 | 'fix',
26 | 'perf',
27 | 'refactor',
28 | 'revert'
29 | ]
30 | ]
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/ApolloHooks/index.html:
--------------------------------------------------------------------------------
1 |
2 | ApolloHooks (ReasonApolloHooks.ApolloHooks) let useQuery: ?client:ApolloClient.generatedApolloClient => ?variables:Js.Json.t => ?notifyOnNetworkStatusChange:bool => ?fetchPolicy:ApolloHooksTypes.fetchPolicy => ?errorPolicy:ApolloHooksTypes.errorPolicy => ?skip:bool => ?pollInterval:int => ApolloHooksTypes.graphqlDefinition ('a , 'b , 'c ) => (Query.variant ('a ), Query.queryResult ('a ));
This is probably the one hook you'll use the most. A quick demo:
open ApolloHooks;
3 |
4 | module Query = [%graphql {|
5 | query MyQuery {
6 | me { id, name }
7 | }
8 | |}];
9 |
10 | [@react.component]
11 | let make = () => {
12 | /* In Reason we prefix variables that we are not going to use with _ */
13 | let (simple, _full) = useQuery(Query.definitions);
14 |
15 | /* When using simple with Reason's pattern-matching operator, the compiler will force you to cover every single branch of the variant type */
16 | switch(simple) {
17 | | Loading => React.string("loading...")
18 | | Error(error) =>
19 | Js.log(error);
20 | React.string("Something went wrong!")
21 | | Data(data) =>
22 | React.string("Hello, " ++ data##me##name)
23 | /* Every. Single. One. Of Them. */
24 | | NoData =>
25 | React.string("Woa something went really wrong! Glady we use Reason and it forced us to handle this! Report this issue")
26 | }
27 | }
Why we return a tuple? While designing and using the API we came to the conclusion that would be much more convient to have a value that would attend the majority of simple usages and a full for when you need to do a complex UI, such as infinite scroll.
The value simple
(Query
.variant('a) ) helps you to consume your data with simplicity, type safety and exhaustiveness check. But for those cases where you really want do do a fine-grained control of your data flow – such as when you have loading
and data
at the same time – that's when full
(Query
.queryResult('a) ) becomes more useful.
module Query = [%graphql {|
28 | query MyQuery {
29 | me { id, name }
30 | }
31 | |}];
32 |
33 | [@react.component]
34 | let make = () => {
35 | let (_simple, full) = useQuery(Query.definitions);
36 |
37 | /* `full` is a record type so you pattern against it's possible combos of values */
38 | switch(full) {
39 | /* Initial loading */
40 | | { loading: true, data: None } => React.string("loading...")
41 | /* Error but no data */
42 | | { loading: false, data: None, error: Some(error) } => React.string("Something went wrong")
43 | /* When we have some data and we tried to refetch but got an error */
44 | | { loading: false, data: Some(data), error: Some(error) } =>
45 | <>
46 | {React.string("Something went wrong")}
47 | <RenderData data onLoadMore={full.refetch} />
48 | </>
49 | /* Just data */
50 | | { loading: false, data: Some(data), error: None } =>
51 | <>
52 | {React.string("Something went wrong")}
53 | <RenderData data onLoadMore={full.refetch} />
54 | </>
55 | | Data(data) =>
56 | React.string("Hello, " ++ data##me##name)
57 | /* Not loading? No data? No error? That's weird */
58 | | {loading: false, data: None, error: null} =>
59 | React.string("Woa something went really wrong! But the programmer remembered to handle this case! Report to us")
60 | }
61 | }
Quite more complex right? Gladly it's not always that we have that level of complexity.
That covers the most common cases of usage. If you want to see more complex usages check out the examples folder.
let useMutation: ?client:ApolloClient.generatedApolloClient => ?variables:Js.Json.t => ?refetchQueries:Mutation.refetchQueries => ?awaitRefetchQueries:bool => ?update:(ApolloClient.generatedApolloClient => Mutation.mutationResult ('a ) => unit) => ?optimisticResponse:Js.Json.t => ApolloHooksTypes.graphqlDefinition ('a , 'b , 'c ) => (Mutation.mutation ('a ), Mutation.controlledVariantResult ('a ), Mutation.controlledResult ('a ));
Second most used! Here's a quick demo:
open ApolloHooks;
62 |
63 | module Mutation = [%graphql {|
64 | mutation MyMutation($input: MyMutationInput!) {
65 | myMutation(input: $input) { error }
66 | }
67 | |}];
68 |
69 | [@react.component]
70 | let make = () => {
71 | /* `simple` and `full` follow the same principle of `useQuery`. */
72 | let (mutate, simple, _full) = useMutation(Mutation.definitions);
73 |
74 | /* When using simple with Reason's pattern-matching operator, the compiler will force you to cover every single branch of the variant type */
75 | switch(simple) {
76 | | Loading => React.string("loading...")
77 | | Error(error) =>
78 | Js.log(error);
79 | React.string("Something went wrong!")
80 | | Data(data) =>
81 | <div>
82 | {React.string("Hello, " ++ data##me##name)}
83 | </div>
84 | /* Every. Single. One. Of Them. */
85 | | NotCalled => <button onClick={_ => mutate()}>{React.string("Click me")}
86 | | NoData =>
87 | React.string("Woa something went really wrong! Glady we use Reason and it forced us to handle this! Report this issue")
88 | }
89 | }
Or if you only care about calling mutate
open ApolloHooks;
90 |
91 | module Mutation = [%graphql {|
92 | mutation MyMutation {
93 | me { id, name }
94 | }
95 | |}];
96 |
97 | [@react.component]
98 | let make = () => {
99 | let (mutate, _simple, _full) = useMutation(Mutation.definitions);
100 | let onClick = _event => {
101 | mutate()
102 | |> Js.Promise.then_(result => {
103 | switch(result) {
104 | | Data(data) => do anything here
105 | | Error(error) => handle your error
106 | | NoData => ...something went wrong...
107 | }
108 | })
109 | }
110 |
111 | <button onClick>{React.string("Click me")}</button>
112 | }
let useSubscription: ?variables:Js.Json.t => ?client:ApolloClient.generatedApolloClient => ApolloHooksTypes.graphqlDefinition ('a , 'b , 'c ) => (Subscription.variant ('a ), Subscription.result ('a ));
useSubscription bindings
let toQueryObj: Js.t({.. query: string, variables: Js.Json.t, }) => ApolloClient.queryObj;
Helper to generate the shape of a query for refetchQueries
mutation param. Take a look in examples/persons/src/EditPerson.re for a more complete demo of usage.
let toReadQueryOptions: Js.t({.. query: string, variables: 'a , }) => Js.t({. query: ReasonApolloTypes.queryString, variables: Js.Nullable.t('a ), });
Helper to generate the shape of a query for ApolloClient.ReadQuery.readQuery
. Used for optimistic UI. Take a look in examples/persons/src/FilterByNameCache.re for a more complete demo of usage.
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/ApolloHooksMutation/index.html:
--------------------------------------------------------------------------------
1 |
2 | ApolloHooksMutation (ReasonApolloHooks.ApolloHooksMutation) type refetchQueries
= ReasonApolloTypes.executionResult => array(ApolloClient.queryObj)
;type result('a)
=
;type controlledResult('a)
=
{
}
;type controlledVariantResult('a)
=
;let gql: ReasonApolloTypes.gql;
type mutationResult('a)
= Js.t({. data: option('a ), })
;include { ... };
type options('a)
;let options: ?variables:Js.Json.t => ?mutation:option(ReasonApolloTypes.queryString) => ?client:ApolloClient.generatedApolloClient => ?refetchQueries:refetchQueries => ?awaitRefetchQueries:bool => ?update:(ApolloClient.generatedApolloClient => mutationResult ('a ) => unit) => ?optimisticResponse:Js.Json.t => unit => options ('a );
let variablesGet: options ('a ) => option(Js.Json.t);
let mutationGet: options ('a ) => option(option(ReasonApolloTypes.queryString));
let clientGet: options ('a ) => option(ApolloClient.generatedApolloClient);
let refetchQueriesGet: options ('a ) => option(refetchQueries );
let awaitRefetchQueriesGet: options ('a ) => option(bool);
let updateGet: options ('a ) => option((ApolloClient.generatedApolloClient => mutationResult ('a ) => unit));
let optimisticResponseGet: options ('a ) => option(Js.Json.t);
type jsResult
= Js.t({. data: Js.Nullable.t(Js.Json.t), loading: bool, called: bool, error: Js.Nullable.t(ApolloHooksTypes.apolloError ), })
;type executionResult
= Js.t({. data: Js.Nullable.t(Js.Json.t), errors: Js.Nullable.t(array(ApolloHooksTypes.graphqlError )), })
;type jsMutate('a)
= Js.Internal.fn([ `Arity_1(options ('a )) ], Js.Promise.t(executionResult ))
;type mutation('a)
= ?variables:Js.Json.t => ?client:ApolloClient.generatedApolloClient => ?refetchQueries:refetchQueries => ?awaitRefetchQueries:bool => ?optimisticResponse:Js.Json.t => unit => Js.Promise.t(result ('a ))
;let useMutationJs: Js.Internal.fn([ `Arity_2((ReasonApolloTypes.queryString, options ('a ))) ], (jsMutate ('a ), jsResult ));
exception
Error (string)
;let useMutation: ?client:ApolloClient.generatedApolloClient => ?variables:Js.Json.t => ?refetchQueries:refetchQueries => ?awaitRefetchQueries:bool => ?update:(ApolloClient.generatedApolloClient => mutationResult ('data ) => unit) => ?optimisticResponse:Js.Json.t => ApolloHooksTypes.graphqlDefinition ('data , 'a , 'b ) => (mutation ('data ), controlledVariantResult ('data ), controlledResult ('data ));
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/ApolloHooksProvider/index.html:
--------------------------------------------------------------------------------
1 |
2 | ApolloHooksProvider (ReasonApolloHooks.ApolloHooksProvider) let makeProps: client:ApolloClient.generatedApolloClient => children:React.element => ?key:string => unit => Js.t({. client: ApolloClient.generatedApolloClient, children: React.element, });
let make: React.componentLike(Js.t({. client: ApolloClient.generatedApolloClient, children: React.element, }), React.element);
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/ApolloHooksQuery/index.html:
--------------------------------------------------------------------------------
1 |
2 | ApolloHooksQuery (ReasonApolloHooks.ApolloHooksQuery) type variant('a)
=
;type updateQueryT
= Js.Json.t => updateQueryOptions => Js.Json.t
;type updateSubscriptionOptionsJs
= Js.t({. subscriptionData: Js.t({. data: Js.Json.t, }), variables: Js.Nullable.t(Js.Json.t), })
;* https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/watchQueryOptions.ts#L139
type updateQuerySubscribeToMoreT
= Js.Json.t => updateSubscriptionOptionsJs => Js.Json.t
;type unsubscribeFnT
= unit => unit
;type refetch('a)
= ?variables:Js.Json.t => unit => Js.Promise.t('a )
;type queryResult('a)
=
{
}
;let gql: ReasonApolloTypes.gql;
include { ... };
type options
;let options: ?variables:Js.Json.t => ?client:ApolloClient.generatedApolloClient => ?notifyOnNetworkStatusChange:bool => ?fetchPolicy:string => ?errorPolicy:string => ?skip:bool => ?pollInterval:int => unit => options ;
let variablesGet: options => option(Js.Json.t);
let clientGet: options => option(ApolloClient.generatedApolloClient);
let notifyOnNetworkStatusChangeGet: options => option(bool);
let fetchPolicyGet: options => option(string);
let errorPolicyGet: options => option(string);
let skipGet: options => option(bool);
let pollIntervalGet: options => option(int);
let useQueryJs: ReasonApolloTypes.queryString => options => Js.t({. data: Js.Nullable.t(Js.Json.t), loading: bool, error: Js.Nullable.t(ApolloHooksTypes.apolloError ), refetch: Js.Internal.meth([ `Arity_1(Js.Nullable.t(Js.Json.t)) ], Js.Promise.t(Js.Json.t)), fetchMore: Js.Internal.meth([ `Arity_1(fetchMoreOptions ) ], Js.Promise.t(unit)), networkStatus: Js.Nullable.t(int), stopPolling: Js.Internal.meth([ `Arity_0 ], unit), startPolling: Js.Internal.meth([ `Arity_1(int) ], unit), subscribeToMore: Js.Internal.meth([ `Arity_1(subscribeToMoreOptionsJs ) ], unsubscribeFnT ), });
let useQuery: ?client:ApolloClient.generatedApolloClient => ?variables:Js.Json.t => ?notifyOnNetworkStatusChange:bool => ?fetchPolicy:ApolloHooksTypes.fetchPolicy => ?errorPolicy:ApolloHooksTypes.errorPolicy => ?skip:bool => ?pollInterval:int => ApolloHooksTypes.graphqlDefinition ('data , 'a , 'b ) => (variant ('data ), queryResult ('data ));
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/ApolloHooksSubscription/index.html:
--------------------------------------------------------------------------------
1 |
2 | ApolloHooksSubscription (ReasonApolloHooks.ApolloHooksSubscription) type error
= Js.t({. message: string, })
;type variant('a)
=
|
Data ('a )
|
Error (error )
|
Loading
|
NoData
;type result('a)
=
{
data: option('a ),
loading: bool,
error: option(error ),
}
;let gql: ReasonApolloTypes.gql;
include { ... };
type options
;let options: ?variables:Js.Json.t => ?skip:bool => ?onSubscriptionData:(unit => unit) => ?client:ApolloClient.generatedApolloClient => unit => options ;
let variablesGet: options => option(Js.Json.t);
let skipGet: options => option(bool);
let onSubscriptionDataGet: options => option((unit => unit));
let clientGet: options => option(ApolloClient.generatedApolloClient);
let useSubscriptionJs: ReasonApolloTypes.queryString => options => Js.t({. data: Js.Nullable.t(Js.Json.t), loading: bool, error: Js.Nullable.t(error ), });
let useSubscription: ?variables:Js.Json.t => ?client:ApolloClient.generatedApolloClient => ApolloHooksTypes.graphqlDefinition ('data , 'a , 'b ) => (variant ('data ), result ('data ));
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/ApolloHooksTypes/index.html:
--------------------------------------------------------------------------------
1 |
2 | ApolloHooksTypes (ReasonApolloHooks.ApolloHooksTypes) type networkStatus
=
|
Loading
|
SetVariables
|
FetchMore
|
Refetch
|
Poll
|
Ready
|
Error
|
Unknown
;* apollo-client/src/core/networkStatus
let toNetworkStatus: Js.Nullable.t(int) => networkStatus ;
type fetchPolicy
=
|
CacheFirst
|
CacheAndNetwork
|
NetworkOnly
|
CacheOnly
|
NoCache
|
Standby
;* apollo-client/src/core/watchQueryOptions.ts
let fetchPolicyToJs: fetchPolicy => string;
type errorPolicy
=
;* apollo-client/src/core/watchQueryOptions.ts
let errorPolicyToJs: errorPolicy => string;
type apolloErrorExtensions
= Js.t({. code: Js.Nullable.t(string), })
;* apollo-client/src/errors/ApolloError.ts
type graphqlError
= Js.t({. message: string, name: Js.Nullable.t(string), extensions: Js.Nullable.t(apolloErrorExtensions ), locations: Js.Nullable.t(array(string)), path: Js.Nullable.t(array(string)), nodes: Js.Nullable.t(array(string)), })
;type apolloError
= Js.t({. message: string, graphQLErrors: Js.Nullable.t(array(graphqlError )), networkError: Js.Nullable.t(string), })
;type parse('a)
= Js.Json.t => 'a
;type query
= string
;type composeVariables('returnType, 'hookReturnType)
= (Js.Json.t => 'returnType ) => 'hookReturnType
;type graphqlDefinition('data, 'returnType, 'hookReturnType)
= (parse ('data ), query , composeVariables ('returnType , 'hookReturnType ))
;
--------------------------------------------------------------------------------
/docs/ReasonApolloHooks/index.html:
--------------------------------------------------------------------------------
1 |
2 | index (ReasonApolloHooks.index) IndexHello!
Check out the module ApolloHooks
to get started.
--------------------------------------------------------------------------------
/docs/highlight.pack.js:
--------------------------------------------------------------------------------
1 | /*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?" ":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/ /g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C=" ",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("ocaml",function(e){return{aliases:["ml"],k:{keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value",built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",literal:"true false"},i:/\/\/|>>/,l:"[a-z_]\\w*!?",c:[{cN:"literal",b:"\\[(\\|\\|)?\\]|\\(\\)",r:0},e.C("\\(\\*","\\*\\)",{c:["self"]}),{cN:"symbol",b:"'[A-Za-z_](?!')[\\w']*"},{cN:"type",b:"`[A-Z][\\w']*"},{cN:"type",b:"\\b[A-Z][\\w']*",r:0},{b:"[a-z_]\\w*'[\\w']*",r:0},e.inherit(e.ASM,{cN:"string",r:0}),e.inherit(e.QSM,{i:null}),{cN:"number",b:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",r:0},{b:/[-=]>/}]}});
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/odoc.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | /* Copyright (c) 2016 The odoc contributors. All rights reserved.
3 | Distributed under the ISC license, see terms at the end of the file.
4 | odoc 1.4.0 */
5 |
6 | /* Fonts */
7 | @import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,500');
8 | @import url('https://fonts.googleapis.com/css?family=Noticia+Text:400,400i,700');
9 | @import url('https://fonts.googleapis.com/css?family=Fira+Sans:400,400i,500,500i,600,600i,700,700i');
10 |
11 |
12 | /* Reset a few things. */
13 |
14 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
15 | margin: 0;
16 | padding: 0;
17 | border: 0;
18 | font-size: inherit;
19 | font: inherit;
20 | line-height: inherit;
21 | vertical-align: baseline;
22 | text-align: inherit;
23 | color: inherit;
24 | background: transparent;
25 | }
26 |
27 | table {
28 | border-collapse: collapse;
29 | border-spacing: 0;
30 | }
31 |
32 | *, *:before, *:after {
33 | box-sizing: border-box;
34 | }
35 |
36 | html {
37 | font-size: 15px;
38 | }
39 |
40 | body {
41 | font-family: "Fira Sans", Helvetica, Arial, sans-serif;
42 | text-align: left;
43 | color: #333;
44 | background: #FFFFFF;
45 | }
46 |
47 | .content {
48 | max-width: 90ex;
49 | margin-left: calc(10vw + 20ex);
50 | margin-right: 4ex;
51 | margin-top: 20px;
52 | margin-bottom: 50px;
53 | font-family: "Noticia Text", Georgia, serif;
54 | line-height: 1.5;
55 | }
56 |
57 | .content>header {
58 | margin-bottom: 30px;
59 | }
60 |
61 | .content>header nav {
62 | font-family: "Fira Sans", Helvetica, Arial, sans-serif;
63 | }
64 |
65 | /* Basic markup elements */
66 |
67 | b, strong {
68 | font-weight: 500;
69 | }
70 |
71 | i, em {
72 | font-style: italic;
73 | }
74 |
75 | sup {
76 | vertical-align: super;
77 | }
78 |
79 | sub {
80 | vertical-align: sub;
81 | }
82 |
83 | sup, sub {
84 | font-size: 12px;
85 | line-height: 0;
86 | margin-left: 0.2ex;
87 | }
88 |
89 | pre {
90 | margin-top: 0.8em;
91 | margin-bottom: 1.2em;
92 | }
93 |
94 | p, ul, ol {
95 | margin-top: 0.5em;
96 | margin-bottom: 1em;
97 | }
98 | ul, ol {
99 | list-style-position: outside
100 | }
101 |
102 | ul>li {
103 | margin-left: 22px;
104 | }
105 |
106 | ol>li {
107 | margin-left: 27.2px;
108 | }
109 |
110 | li>*:first-child {
111 | margin-top: 0
112 | }
113 |
114 | /* Text alignements, this should be forbidden. */
115 |
116 | .left {
117 | text-align: left;
118 | }
119 |
120 | .right {
121 | text-align: right;
122 | }
123 |
124 | .center {
125 | text-align: center;
126 | }
127 |
128 | /* Links and anchors */
129 |
130 | a {
131 | text-decoration: none;
132 | color: #2C5CBD;
133 | }
134 |
135 | a:hover {
136 | box-shadow: 0 1px 0 0 #2C5CBD;
137 | }
138 |
139 | /* Linked highlight */
140 | *:target {
141 | background-color: rgba(187,239,253,0.3) !important;
142 | box-shadow: 0 0px 0 1px rgba(187,239,253,0.8) !important;
143 | border-radius: 1px;
144 | }
145 |
146 | *:hover>a.anchor {
147 | visibility: visible;
148 | }
149 |
150 | a.anchor:before {
151 | content: "#"
152 | }
153 |
154 | a.anchor:hover {
155 | box-shadow: none;
156 | text-decoration: none;
157 | color: #555;
158 | }
159 |
160 | a.anchor {
161 | visibility: hidden;
162 | position: absolute;
163 | /* top: 0px; */
164 | /* margin-left: -3ex; */
165 | margin-left: -1.3em;
166 | font-weight: normal;
167 | font-style: normal;
168 | padding-right: 0.4em;
169 | padding-left: 0.4em;
170 | /* To remain selectable */
171 | color: #d5d5d5;
172 | }
173 |
174 | .spec > a.anchor {
175 | margin-left: -2.3em;
176 | padding-right: 0.9em;
177 | }
178 |
179 | .xref-unresolved {
180 | color: #2C5CBD;
181 | }
182 | .xref-unresolved:hover {
183 | box-shadow: 0 1px 0 0 #CC6666;
184 | }
185 |
186 | /* Section and document divisions.
187 | Until at least 4.03 many of the modules of the stdlib start at .h7,
188 | we restart the sequence there like h2 */
189 |
190 | h1, h2, h3, h4, h5, h6, .h7, .h8, .h9, .h10 {
191 | font-family: "Fira Sans", Helvetica, Arial, sans-serif;
192 | font-weight: 400;
193 | margin: 0.5em 0 0.5em 0;
194 | padding-top: 0.1em;
195 | line-height: 1.2;
196 | overflow-wrap: break-word;
197 | }
198 |
199 | h1 {
200 | font-weight: 500;
201 | font-size: 2.441em;
202 | margin-top: 1.214em;
203 | }
204 |
205 | h1 {
206 | font-weight: 500;
207 | font-size: 1.953em;
208 | box-shadow: 0 1px 0 0 #ddd;
209 | }
210 |
211 | h2 {
212 | font-size: 1.563em;
213 | }
214 |
215 | h3 {
216 | font-size: 1.25em;
217 | }
218 |
219 | small, .font_small {
220 | font-size: 0.8em;
221 | }
222 |
223 | h1 code, h1 tt {
224 | font-size: inherit;
225 | font-weight: inherit;
226 | }
227 |
228 | h2 code, h2 tt {
229 | font-size: inherit;
230 | font-weight: inherit;
231 | }
232 |
233 | h3 code, h3 tt {
234 | font-size: inherit;
235 | font-weight: inherit;
236 | }
237 |
238 | h3 code, h3 tt {
239 | font-size: inherit;
240 | font-weight: inherit;
241 | }
242 |
243 | h4 {
244 | font-size: 1.12em;
245 | }
246 |
247 |
248 | /* Preformatted and code */
249 |
250 | tt, code, pre {
251 | font-family: "Fira Mono", courier;
252 | font-weight: 400;
253 | }
254 |
255 | pre {
256 | padding: 0.1em;
257 | border: 1px solid #eee;
258 | border-radius: 5px;
259 | overflow-x: auto;
260 | }
261 |
262 | p code, li code {
263 | background-color: #f6f8fa;
264 | color: #0d2b3e;
265 | border-radius: 3px;
266 | padding: 0 0.3ex;
267 | }
268 |
269 | p a > code {
270 | color: #2C5CBD;
271 | }
272 |
273 | /* Code blocks (e.g. Examples) */
274 |
275 | pre code {
276 | font-size: 0.893rem;
277 | }
278 |
279 | /* Code lexemes */
280 |
281 | .keyword {
282 | font-weight: 500;
283 | }
284 |
285 | /* Module member specification */
286 |
287 | .spec:not(.include), .spec.include details summary {
288 | background-color: #f6f8fa;
289 | border-radius: 3px;
290 | border-left: 4px solid #5c9cf5;
291 | border-right: 5px solid transparent;
292 | padding: 0.35em 0.5em;
293 | }
294 |
295 | .spec.include details summary:hover {
296 | background-color: #ebeff2;
297 | }
298 |
299 | dl, div.spec, .doc, aside {
300 | margin-bottom: 20px;
301 | }
302 |
303 | dl > dd {
304 | padding: 0.5em;
305 | }
306 |
307 | dd> :first-child {
308 | margin-top: 0;
309 | }
310 |
311 | dl:last-child, dd> :last-child, aside:last-child, article:last-child {
312 | margin-bottom: 0;
313 | }
314 |
315 | dt+dt {
316 | margin-top: 15px;
317 | }
318 |
319 | section+section, section > header + dl {
320 | margin-top: 25px;
321 | }
322 |
323 | .spec.type .variant {
324 | margin-left: 2ch;
325 | }
326 | .spec.type .variant p {
327 | margin: 0;
328 | font-style: italic;
329 | }
330 | .spec.type .record {
331 | margin-left: 2ch;
332 | }
333 | .spec.type .record p {
334 | margin: 0;
335 | font-style: italic;
336 | }
337 |
338 | div.def {
339 | margin-top: 0;
340 | text-indent: -2ex;
341 | padding-left: 2ex;
342 | }
343 |
344 | div.def+div.doc {
345 | margin-left: 1ex;
346 | margin-top: 2.5px
347 | }
348 |
349 | div.doc>*:first-child {
350 | margin-top: 0;
351 | }
352 |
353 | /* The elements other than heading should be wrapped in elements. */
354 | /* heading, body>p, body>ul, body>ol, h3, h4, body>pre { */
355 | /* margin-bottom: 30px; */
356 | /* } */
357 |
358 | /* Collapsible inlined include and module */
359 |
360 | .spec.include details {
361 | position: relative;
362 | }
363 |
364 | .spec.include details:after {
365 | z-index: -100;
366 | display: block;
367 | content: " ";
368 | position: absolute;
369 | border-radius: 0 1ex 1ex 0;
370 | right: -20px;
371 | top: 1px;
372 | bottom: 1px;
373 | width: 15px;
374 | background: rgba(0, 4, 15, 0.05);
375 | box-shadow: 0 0px 0 1px rgba(204, 204, 204, 0.53);
376 | }
377 |
378 | .spec.include details summary {
379 | position: relative;
380 | margin-bottom: 20px;
381 | cursor: pointer;
382 | outline: none;
383 | }
384 |
385 | /* FIXME: Does not work in Firefox. */
386 | details summary::-webkit-details-marker {
387 | color: #888;
388 | transform: scaleX(-1);
389 | position: absolute;
390 | top: calc(50% - 5px);
391 | height: 11px;
392 | right: -29px;
393 | }
394 |
395 | /* Records and variants FIXME */
396 |
397 | div.def table {
398 | text-indent: 0em;
399 | padding: 0;
400 | margin-left: -2ex;
401 | }
402 |
403 | td.def {
404 | padding-right: 2ex
405 | }
406 |
407 | .record td.def {
408 | padding-left: 2ex;
409 | }
410 |
411 | td.doc *:first-child {
412 | margin-top: 0em
413 | }
414 |
415 | /* @ tags */
416 |
417 | ul.at-tag {
418 | list-style-type: none;
419 | margin-left: 0;
420 | padding: 0;
421 | }
422 |
423 | ul.at-tag li {
424 | margin-left: 0;
425 | padding: 0;
426 | }
427 |
428 | ul.at-tag li p:first-child {
429 | margin-top: 0
430 | }
431 |
432 | /* FIXME remove */
433 |
434 | span.at-tag {
435 | font-weight: bold
436 | }
437 |
438 | .at-tag.deprecated {
439 | font-weight: normal;
440 | color: crimson
441 | }
442 |
443 | .at-tag.raise {
444 | font-weight: bold;
445 | }
446 |
447 | /* FIXME random other things to review. */
448 |
449 | .heading {
450 | margin-top: 10px;
451 | border-bottom: solid;
452 | border-width: 1px;
453 | border-color: #DDD;
454 | text-align: right;
455 | font-weight: normal;
456 | font-style: italic;
457 | }
458 |
459 | .heading+.sig {
460 | margin-top: -20px;
461 | }
462 |
463 | .heading+.parameters {
464 | margin-top: -20px;
465 | }
466 |
467 | /* Odig package index */
468 |
469 | .by-name ol, .by-tag ol, .errors ol {
470 | list-style-type: none;
471 | margin-left: 0;
472 | }
473 |
474 | .by-name ol ol, .by-tag ol ol {
475 | margin-top: 0;
476 | margin-bottom: 0
477 | }
478 |
479 | .by-name li, .by-tag li, .errors li {
480 | margin-left: 0;
481 | }
482 |
483 | .by-name .version {
484 | font-size: 10px;
485 | color: #AAA
486 | }
487 |
488 | .by-name nav {
489 | margin-bottom: 10px
490 | }
491 |
492 | .by-name nav a {
493 | text-transform: uppercase;
494 | font-size: 18px;
495 | margin-right: 1ex;
496 | color: #222;
497 | display: inline-block;
498 | }
499 |
500 | .by-tag nav a {
501 | margin-right: 1ex;
502 | color: #222;
503 | display: inline-block;
504 | }
505 |
506 | .by-tag>ol>li {
507 | margin-top: 10px;
508 | }
509 |
510 | .by-tag>ol>li>span, .by-tag>ol>li>ol, .by-tag>ol>li>ol>li {
511 | display: inline-block;
512 | margin-right: 1ex;
513 | }
514 |
515 | /* Odig package page */
516 |
517 | .package nav {
518 | display: inline;
519 | font-size: 14px;
520 | font-weight: normal;
521 | }
522 |
523 | .package .version {
524 | font-size: 14px;
525 | }
526 |
527 | h1+.modules, h1+.sel {
528 | margin-top: 10px
529 | }
530 |
531 | .sel {
532 | font-weight: normal;
533 | font-style: italic;
534 | font-size: 14px;
535 | margin-top: 20px;
536 | }
537 |
538 | .sel+.modules {
539 | margin-top: 10px;
540 | margin-bottom: 20px;
541 | margin-left: 1ex;
542 | }
543 |
544 | .modules {
545 | margin: 0;
546 | }
547 |
548 | .modules .module {
549 | min-width: 8ex;
550 | padding-right: 2ex
551 | }
552 |
553 | .package.info {
554 | margin: 0;
555 | }
556 |
557 | .package.info td:first-child {
558 | font-style: italic;
559 | padding-right: 2ex;
560 | }
561 |
562 | .package.info ul {
563 | list-style-type: none;
564 | display: inline;
565 | margin: 0;
566 | }
567 |
568 | .package.info li {
569 | display: inline-block;
570 | margin: 0;
571 | margin-right: 1ex;
572 | }
573 |
574 | #info-authors li, #info-maintainers li {
575 | display: block;
576 | }
577 |
578 | /* Sidebar and TOC */
579 |
580 | .toc:before {
581 | display: block;
582 | content: "Contents";
583 | text-transform: uppercase;
584 | font-size: 1em;
585 | margin: 1.414em 0 0.5em;
586 | font-weight: 500;
587 | color: #777;
588 | line-height: 1.2;
589 | }
590 |
591 | .toc {
592 | position: fixed;
593 | top: 0px;
594 | bottom: 0px;
595 | left: 0px;
596 | max-width: 30ex;
597 | min-width: 26ex;
598 | width: 20%;
599 | background: #f6f8fa;
600 | overflow: auto;
601 | color: #1F2D3D;
602 | padding-left: 2ex;
603 | padding-right: 2ex;
604 | }
605 |
606 | .toc ul li a {
607 | font-family: "Fira Sans", sans-serif;
608 | font-size: 0.95em;
609 | color: #333;
610 | font-weight: 400;
611 | line-height: 1.6em;
612 | display: block;
613 | }
614 |
615 | .toc ul li a:hover {
616 | box-shadow: none;
617 | text-decoration: underline;
618 | }
619 |
620 | /* First level titles */
621 |
622 | .toc>ul>li>a {
623 | font-weight: 500;
624 | }
625 |
626 | .toc li ul {
627 | margin: 0px;
628 | }
629 |
630 | .toc ul {
631 | list-style-type: none;
632 | }
633 |
634 | .toc ul li {
635 | margin: 0;
636 | }
637 | .toc>ul>li {
638 | margin-bottom: 0.3em;
639 | }
640 |
641 | .toc ul li li {
642 | border-left: 1px solid #ccc;
643 | margin-left: 5px;
644 | padding-left: 12px;
645 | }
646 |
647 | /* Mobile adjustements. */
648 |
649 | @media only screen and (max-width: 95ex) {
650 | .content {
651 | margin: auto;
652 | padding: 2.0em;
653 | }
654 | .toc {
655 | position: static;
656 | width: auto;
657 | min-width: unset;
658 | max-width: unset;
659 | border: none;
660 | padding: 0.2em 1em;
661 | border-radius: 5px;
662 | }
663 | }
664 |
665 | /* Print adjustements. */
666 |
667 | @media print {
668 | body {
669 | color: black;
670 | background: white;
671 | }
672 | body nav:first-child {
673 | visibility: hidden;
674 | }
675 | }
676 |
677 | /* Syntax highlighting (based on github-gist) */
678 |
679 | .hljs {
680 | display: block;
681 | background: white;
682 | padding: 0.5em;
683 | color: #333333;
684 | overflow-x: auto;
685 | }
686 |
687 | .hljs-comment,
688 | .hljs-meta {
689 | color: #969896;
690 | }
691 |
692 | .hljs-string,
693 | .hljs-variable,
694 | .hljs-template-variable,
695 | .hljs-strong,
696 | .hljs-emphasis,
697 | .hljs-quote {
698 | color: #df5000;
699 | }
700 |
701 | .hljs-keyword,
702 | .hljs-selector-tag {
703 | color: #a71d5d;
704 | }
705 |
706 | .hljs-type,
707 | .hljs-class .hljs-title {
708 | color: #458;
709 | font-weight: 500;
710 | }
711 |
712 | .hljs-literal,
713 | .hljs-symbol,
714 | .hljs-bullet,
715 | .hljs-attribute {
716 | color: #0086b3;
717 | }
718 |
719 | .hljs-section,
720 | .hljs-name {
721 | color: #63a35c;
722 | }
723 |
724 | .hljs-tag {
725 | color: #333333;
726 | }
727 |
728 | .hljs-attr,
729 | .hljs-selector-id,
730 | .hljs-selector-class,
731 | .hljs-selector-attr,
732 | .hljs-selector-pseudo {
733 | color: #795da3;
734 | }
735 |
736 | .hljs-addition {
737 | color: #55a532;
738 | background-color: #eaffea;
739 | }
740 |
741 | .hljs-deletion {
742 | color: #bd2c00;
743 | background-color: #ffecec;
744 | }
745 |
746 | .hljs-link {
747 | text-decoration: underline;
748 | }
749 |
750 | /*---------------------------------------------------------------------------
751 | Copyright (c) 2016 The odoc contributors
752 |
753 | Permission to use, copy, modify, and/or distribute this software for any
754 | purpose with or without fee is hereby granted, provided that the above
755 | copyright notice and this permission notice appear in all copies.
756 |
757 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
758 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
759 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
760 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
761 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
762 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
763 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
764 | ---------------------------------------------------------------------------*/
765 |
--------------------------------------------------------------------------------
/examples/persons/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .merlin
3 | .bsb.lock
4 | npm-debug.log
5 | /lib/bs/
6 | /node_modules/
7 | .graphql_ppx_cache
8 |
--------------------------------------------------------------------------------
/examples/persons/README.md:
--------------------------------------------------------------------------------
1 | # persons
2 |
3 | ## Run Persons Example
4 |
5 | ```sh
6 | npm i
7 | npm start
8 | # in another tab
9 | npm run server
10 | ```
11 |
12 | When you make changes to the bindings in `reason-apollo-hooks`, the changes won't automatically propagate to the code in the example project, so you won't see any updates while it is running. This happens because there is still a previous local version of `reason-apollo-hooks` in `node_modules` (referenced as `"reason-apollo-hooks": "../../"` in `package.json`).
13 |
14 | To get the updates, first build `reason-apollo-hooks`:
15 |
16 | ```sh
17 | npm run clean
18 | npm run build
19 | ```
20 |
21 | Then run `npm run update-deps`, which will remove the old versions of `reason-apollo-hooks` and get the latest local changes:
22 |
23 | ```sh
24 | npm run update-deps
25 | npm run clean
26 | npm start
27 | ```
28 |
29 | Since `reason-apollo-hooks` uses `react` and `reason-react` as peer dependencies, you might end up with duplicated `react` packages in `node_modules` due to `yarn`/`npm` not being able to handle them properly while developing locally. This will cause a runtime error. To remove those packages, run `npm run dedupe:react`.
30 |
31 | ## Changelog
32 | Check the releases page for releasing notes
33 |
--------------------------------------------------------------------------------
/examples/persons/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hooks-template",
3 | "reason": {
4 | "react-jsx": 3
5 | },
6 | "sources": {
7 | "dir": "src",
8 | "subdirs": true
9 | },
10 | "package-specs": [
11 | {
12 | "module": "commonjs",
13 | "in-source": true
14 | }
15 | ],
16 | "suffix": ".bs.js",
17 | "namespace": true,
18 | "ppx-flags": ["@baransu/graphql_ppx_re/ppx6"],
19 | "bs-dependencies": ["reason-react", "reason-apollo", "reason-apollo-hooks"],
20 | "refmt": 3
21 | }
22 |
--------------------------------------------------------------------------------
/examples/persons/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reason Apollo Hooks example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/persons/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "persons",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "build": "bsb -clean-world -make-world",
6 | "start": "bsb -clean-world -make-world -w",
7 | "clean": "bsb -clean-world",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "webpack": "webpack -w",
10 | "webpack:production": "NODE_ENV=production webpack",
11 | "server": "webpack-dev-server",
12 | "update-deps": "rm -rf node_modules/reason-apollo-hooks && yarn install --check-files && npm run dedupe:react",
13 | "dedupe:react": "npm dedupe reason-react --no-package-lock"
14 | },
15 | "keywords": [
16 | "BuckleScript"
17 | ],
18 | "author": "",
19 | "license": "MIT",
20 | "dependencies": {
21 | "@apollo/react-hooks": "^3.1.5",
22 | "react": "^16.10.1",
23 | "react-dom": "^16.10.1",
24 | "reason-apollo": "^0.19.0",
25 | "reason-apollo-hooks": "../../",
26 | "reason-react": ">=0.8.0"
27 | },
28 | "devDependencies": {
29 | "bs-platform": "^7.3.2",
30 | "css-loader": "^3.2.0",
31 | "@baransu/graphql_ppx_re": "^0.7.1",
32 | "html-webpack-plugin": "^3.2.0",
33 | "style-loader": "^1.0.0",
34 | "webpack": "^4.0.1",
35 | "webpack-cli": "^3.1.1",
36 | "webpack-dev-server": "^3.1.8"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/persons/src/AddPerson.re:
--------------------------------------------------------------------------------
1 | open ApolloHooks;
2 |
3 | module EditPersonMutation = [%graphql
4 | {|
5 | mutation addPerson($age: Int!, $name: String!) {
6 | createPerson(age: $age, name: $name) {
7 | id
8 | age
9 | name
10 | }
11 | }
12 | |}
13 | ];
14 |
15 | [@react.component]
16 | let make = () => {
17 | let (name, setName) = React.useState(_ => "");
18 | let (age, setAge) = React.useState(_ => 0);
19 |
20 | let (addPersonMutation, _simple, _full) =
21 | useMutation(EditPersonMutation.definition);
22 |
23 | let handleSubmit = event => {
24 | ReactEvent.Form.preventDefault(event);
25 | addPersonMutation(
26 | ~variables=EditPersonMutation.makeVariables(~age, ~name, ()),
27 | (),
28 | )
29 | |> ignore;
30 | };
31 |
32 | ;
64 | };
65 |
--------------------------------------------------------------------------------
/examples/persons/src/Client.re:
--------------------------------------------------------------------------------
1 | let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();
2 |
3 | let httpLink =
4 | ApolloLinks.createHttpLink(
5 | ~uri="https://api.graph.cool/simple/v1/cjdgba1jw4ggk0185ig4bhpsn",
6 | (),
7 | );
8 |
9 | /* WebSocket client */
10 | let webSocketLink =
11 | ApolloLinks.webSocketLink({
12 | uri: "wss://subscriptions.graph.cool/v1/cjdgba1jw4ggk0185ig4bhpsn",
13 | options: {
14 | reconnect: true,
15 | connectionParams: None,
16 | },
17 | });
18 |
19 | /* Using the ability to split links, you can send data to each link
20 | depending on what kind of operation is being sent */
21 | let webSocketHttpLink =
22 | ApolloLinks.split(
23 | operation => {
24 | let operationDefition =
25 | ApolloUtilities.getMainDefinition(operation.query);
26 | operationDefition.kind == "OperationDefinition"
27 | && operationDefition.operation == "subscription";
28 | },
29 | webSocketLink,
30 | httpLink,
31 | );
32 |
33 | let instance =
34 | ReasonApollo.createApolloClient(
35 | ~link=webSocketHttpLink,
36 | ~cache=inMemoryCache,
37 | (),
38 | );
39 |
--------------------------------------------------------------------------------
/examples/persons/src/EditPerson.re:
--------------------------------------------------------------------------------
1 | open ApolloHooks;
2 | module EditPersonMutation = [%graphql
3 | {|
4 | mutation updatePerson($id: ID!, $age: Int!, $name: String!) {
5 | updatePerson(id: $id, age: $age, name: $name) {
6 | id
7 | age
8 | name
9 | }
10 | }
11 | |}
12 | ];
13 |
14 | module OptimisticResponse = {
15 | /* We need to manually serialise the mutation response into an object that apollo-client understands.
16 | * See the docs here https://www.apollographql.com/docs/react/performance/optimistic-ui/
17 | *
18 | * There is a PR at graphql_ppx_re (https://github.com/baransu/graphql_ppx_re/pull/20) that adds a
19 | * serialisation function directly to the generated mutation. That should make this step unnecessary for most
20 | * usecases.
21 | */
22 | type t = {
23 | .
24 | "__typename": string,
25 | "updatePerson": {
26 | .
27 | "__typename": string,
28 | "age": int,
29 | "id": string,
30 | "name": string,
31 | },
32 | };
33 |
34 | external cast: t => Js.Json.t = "%identity";
35 |
36 | let make = (~id, ~name, ~age) =>
37 | {
38 | "__typename": "Mutation",
39 | "updatePerson": {
40 | "__typename": "Person",
41 | "id": id,
42 | "name": name,
43 | "age": age,
44 | },
45 | }
46 | ->cast;
47 | };
48 |
49 | type state = {
50 | id: string,
51 | age: option(int),
52 | name: string,
53 | };
54 |
55 | type action =
56 | | SetId(string)
57 | | SetAge(option(int))
58 | | SetName(string);
59 |
60 | let reducer = (state, action) => {
61 | switch (action) {
62 | | SetId(id) => {...state, id}
63 | | SetAge(age) => {...state, age}
64 | | SetName(name) => {...state, name}
65 | };
66 | };
67 |
68 | [@react.component]
69 | let make = (~refetchQueries, ~update) => {
70 | let (state, dispatch) =
71 | React.useReducer(reducer, {age: None, name: "", id: ""});
72 |
73 | let (editPersonMutation, _simple, _full) =
74 | useMutation(
75 | ~refetchQueries,
76 | ~update,
77 | ~errorPolicy=All, // See note below on error policies
78 | EditPersonMutation.definition,
79 | );
80 |
81 | let handleSubmit = event => {
82 | ReactEvent.Form.preventDefault(event);
83 | switch (state.age) {
84 | | Some(age) =>
85 | editPersonMutation(
86 | ~variables=
87 | EditPersonMutation.makeVariables(
88 | ~age,
89 | ~id=state.id,
90 | ~name=state.name,
91 | (),
92 | ),
93 | ~optimisticResponse=
94 | OptimisticResponse.make(~id=state.id, ~name=state.name, ~age),
95 | (),
96 | )
97 | /* Setting error policy to All (or Ignore) means that errors show up
98 | * in then_ in the promise returned by editPersonMutation
99 | * Not setting it (or setting it to None) makes the promise reject
100 | * on errors and you'll have to handle errors in Js.catch(e => ...) instead,
101 | * where e is just Js.Promise.error and you won't get any help from the type system.
102 | *
103 | * See also: https://www.apollographql.com/docs/react/data/error-handling/#error-policies
104 | */
105 | |> Js.Promise.then_(((simple, _full) as result) => {
106 | switch (simple) {
107 | | ApolloHooks.Mutation.Errors(_theErrors) => Js.log("OH NO!")
108 | | NoData => Js.log("NO DATA?")
109 | | Data(_theData) => Js.log("DATA!")
110 | };
111 | // If you don't need to handle the result elsewhere,
112 | // the promise can just resolve to unit
113 | Js.Promise.resolve(result);
114 | })
115 | |> ignore
116 | | None => ()
117 | };
118 | };
119 |
120 | ;
168 | };
169 |
--------------------------------------------------------------------------------
/examples/persons/src/FilterByAge.re:
--------------------------------------------------------------------------------
1 | open ApolloHooks;
2 |
3 | module PersonsOlderThanQuery = [%graphql
4 | {|
5 | query getPersonsOlderThan($age: Int!) {
6 | allPersons(filter: { age_gte: $age } ) {
7 | id
8 | }
9 | }
10 | |}
11 | ];
12 |
13 | [@react.component]
14 | let make = (~age) => {
15 | let (simple, _full) =
16 | useQuery(
17 | ~variables=PersonsOlderThanQuery.makeVariables(~age, ()),
18 | PersonsOlderThanQuery.definition,
19 | );
20 |
21 |
22 | {switch (simple) {
23 | | Loading =>
{React.string("Loading...")}
24 | | Data(data) =>
25 |
26 | {"There are "
27 | ++ (data##allPersons->Belt.Array.length |> string_of_int)
28 | ++ " people older than "
29 | ++ string_of_int(age)
30 | |> React.string}
31 |
32 | | NoData
33 | | Error(_) =>
{React.string("Error")}
34 | }}
35 |
;
36 | };
37 |
--------------------------------------------------------------------------------
/examples/persons/src/FilterByAgeErrorHandling.re:
--------------------------------------------------------------------------------
1 | // To Test this component, turn off your internet connection or disable wifi to see the error message produced in the browser
2 |
3 | open ApolloHooks;
4 |
5 | module PersonsOlderThanQuery = [%graphql
6 | {|
7 | query getPersonsOlderThan($age: Int!) {
8 | allPersons(filter: { age_gte: $age } ) {
9 | id
10 | }
11 | }
12 | |}
13 | ];
14 |
15 | [@react.component]
16 | let make = (~age) => {
17 | let (simple, _full) =
18 | useQuery(
19 | ~errorPolicy=All,
20 | ~variables=PersonsOlderThanQuery.makeVariables(~age, ()),
21 | PersonsOlderThanQuery.definition,
22 | );
23 |
24 |
25 | {switch (simple) {
26 | | Loading =>
{React.string("Loading...")}
27 | | Data(data) =>
28 |
29 | {"There are "
30 | ++ (data##allPersons->Belt.Array.length |> string_of_int)
31 | ++ " people older than "
32 | ++ string_of_int(age)
33 | |> React.string}
34 |
35 | | NoData => React.null
36 | | Error(error) =>
{React.string(error##message)}
37 | }}
38 |
;
39 | };
--------------------------------------------------------------------------------
/examples/persons/src/FilterByNameCache.re:
--------------------------------------------------------------------------------
1 | open ApolloHooks;
2 |
3 | /**
4 | * Query response will be parsed using Config.parse from graphq_ppx before it is accessed in
5 | * reason, but react-apollo will save it in cache in its original format, as a regular JS object,
6 | * and apollo requires the data to be saved in cache in the same format or cache won't work correctly.
7 | *
8 | * If using directives like @bsRecord, @bsDecoder or @bsVariant on the query result, the data
9 | * in cache and the parsed data won't to have the same format. Since there is currently no way
10 | * to serialize the parsed data back to its initial format, queries that will be updated manually
11 | * in cache can't use any of those directive, unless you will take care of the serialization yourself.
12 | */
13 | module PersonsNameFilterQuery = [%graphql
14 | {|
15 | query getPersonsWithName($name: String!) {
16 | allPersons(filter: { name: $name } ) {
17 | id
18 | name
19 | age
20 | }
21 | }
22 | |}
23 | ];
24 |
25 | external cast: Js.Json.t => PersonsNameFilterQuery.t = "%identity";
26 |
27 | type person = {
28 | .
29 | "age": int,
30 | "id": string,
31 | "name": string,
32 | };
33 |
34 | /** example using cache */
35 | module PersonsNameFilterReadQuery =
36 | ApolloClient.ReadQuery(PersonsNameFilterQuery);
37 |
38 | module PersonsNameFilterWriteQuery =
39 | ApolloClient.WriteQuery(PersonsNameFilterQuery);
40 |
41 | let updateFiltered = (person: person, name, filteredPersons: array(person)) =>
42 | person##name === name
43 | ? filteredPersons->Belt.Array.concat([|person|])
44 | : filteredPersons->Belt.Array.keep(p => p##id !== person##id);
45 |
46 | let updateCache = (client, person, name) => {
47 | let filterByNameQuery = PersonsNameFilterQuery.make(~name, ());
48 |
49 | // By default, apollo adds field __typename to the query and will use it
50 | // to normalize data. Parsing the result with Config.parse will remove the field,
51 | // which won't allow to save the data back to cache. This means we can't use ReadQuery.make,
52 | // which parses cache result, and have to use the readQuery which returns Json.t.
53 | switch (
54 | PersonsNameFilterReadQuery.readQuery(
55 | client,
56 | {
57 | query: ApolloClient.gql(. filterByNameQuery##query),
58 | variables: Js.Nullable.return(filterByNameQuery##variables),
59 | },
60 | )
61 | ) {
62 | | exception _ => ()
63 | | cachedResponse =>
64 | switch (cachedResponse |> Js.Nullable.toOption) {
65 | | None => ()
66 | | Some(cachedPersons) =>
67 | // readQuery returns unparsed data as Json.t, but since PersonsNameFilterQuery
68 | // is not using any graphql_ppx directive, the data will have the same format,
69 | // (with the addition of __typename field) and can be cast to PersonsNameFilterConfig.t.
70 | let persons = cast(cachedPersons);
71 | let updatedPersons = {
72 | "allPersons": updateFiltered(person, name, persons##allPersons),
73 | };
74 |
75 | PersonsNameFilterWriteQuery.make(
76 | ~client,
77 | ~variables=filterByNameQuery##variables,
78 | ~data=updatedPersons,
79 | (),
80 | );
81 | }
82 | };
83 | };
84 |
85 | [@react.component]
86 | let make = (~name) => {
87 | let (simple, _full) =
88 | useQuery(
89 | ~variables=PersonsNameFilterQuery.make(~name, ())##variables,
90 | PersonsNameFilterQuery.definition,
91 | );
92 |
93 |
94 | {switch (simple) {
95 | | Loading =>
{React.string("Loading...")}
96 | | Data(data) =>
97 |
98 | {"There are "
99 | ++ (data##allPersons->Belt.Array.length |> string_of_int)
100 | ++ " with name "
101 | ++ name
102 | |> React.string}
103 |
104 | | NoData
105 | | Error(_) =>
{React.string("Error")}
106 | }}
107 |
;
108 | };
109 |
--------------------------------------------------------------------------------
/examples/persons/src/Index.re:
--------------------------------------------------------------------------------
1 | [%bs.raw {|require('./styles.css')|}];
2 |
3 | ReactDOMRe.renderToElementWithId(
4 |
5 |
6 |
7 |
8 | ,
9 | "root",
10 | );
11 |
--------------------------------------------------------------------------------
/examples/persons/src/LoadMore.re:
--------------------------------------------------------------------------------
1 | module GetAllPersonsQuery = [%graphql
2 | {|
3 | query getAllPersons($skip: Int!, $first: Int!) {
4 | allPersons(skip: $skip, first: $first) {
5 | id
6 | age
7 | name
8 | }
9 | }
10 | |}
11 | ];
12 |
13 | let personsPerPage = 5;
14 |
15 | [@react.component]
16 | let make = () => {
17 | let (_simple, full) =
18 | ApolloHooks.useQuery(
19 | ~variables=
20 | GetAllPersonsQuery.makeVariables(~skip=0, ~first=personsPerPage, ()),
21 | ~notifyOnNetworkStatusChange=true,
22 | GetAllPersonsQuery.definition,
23 | );
24 |
25 | let handleLoadMore = _ => {
26 | let skip =
27 | switch (full) {
28 | | {data: Some(data)} => data##allPersons->Belt.Array.length
29 | | _ => 0
30 | };
31 |
32 | full.fetchMore(
33 | ~variables=
34 | GetAllPersonsQuery.makeVariables(~skip, ~first=personsPerPage, ()),
35 | ~updateQuery=[%bs.raw
36 | {|
37 | function(prevResult, { fetchMoreResult, ...rest }) {
38 | if (!fetchMoreResult) return prevResult;
39 | return {
40 | ...fetchMoreResult,
41 | allPersons: prevResult.allPersons.concat(fetchMoreResult.allPersons)
42 | };
43 | }
44 | |}
45 | ],
46 | (),
47 | )
48 | |> ignore;
49 | };
50 |
51 |
52 | {switch (full) {
53 | | {loading: true, data: None} =>
{React.string("Loading...")}
54 | | {data: Some(data)} =>
55 | <>
56 |
57 |
59 | {React.string("Load more")}
60 |
61 | >
62 | | {error: Some(_)} =>
{React.string("Error")}
63 | | {error: None, data: None, loading: false} =>
64 |
{React.string("Not asked")}
65 | }}
66 |
;
67 | };
68 |
--------------------------------------------------------------------------------
/examples/persons/src/Persons.re:
--------------------------------------------------------------------------------
1 | [@react.component]
2 | let make = (~persons) =>
3 |
4 | {persons->Belt.Array.map(person =>
5 |
6 |
7 | {React.string("Id: ")}
8 | {React.string(person##id)}
9 |
10 |
11 | {React.string("Name: ")}
12 | {React.string(person##name)}
13 |
14 |
15 | {React.string("Age: ")}
16 | {person##age |> string_of_int |> React.string}
17 |
18 |
19 | )
20 | |> React.array}
21 |
;
22 |
--------------------------------------------------------------------------------
/examples/persons/src/Root.re:
--------------------------------------------------------------------------------
1 | open ApolloHooks;
2 |
3 | let filterAgeLimit = 42;
4 | let filterName = "Bob";
5 |
6 | type example =
7 | | LoadMore
8 | | LoadMoreFragments
9 | | SubscribeToMore;
10 |
11 | [@react.component]
12 | let make = () => {
13 | let (activeExample, setActiveExample) = React.useState(_ => LoadMore);
14 |
15 | let editPersonRefetchQueries = _ => {
16 | let query =
17 | FilterByAge.PersonsOlderThanQuery.make(~age=filterAgeLimit, ());
18 | [|toQueryObj(query)|];
19 | };
20 |
21 | let editPersonUpdate = (client, mutationResult) => {
22 | let data =
23 | mutationResult##data
24 | ->Belt.Option.flatMap(result => result##updatePerson);
25 | switch (data) {
26 | | Some(person) =>
27 | FilterByNameCache.updateCache(client, person, filterName)
28 | | None => ()
29 | };
30 | };
31 |
32 | let getTabClassName = tabExample =>
33 | "tab" ++ (tabExample == activeExample ? " selected-tab" : "");
34 |
35 | <>
36 |
37 | setActiveExample(_ => LoadMore)}>
40 | {React.string("Load More")}
41 |
42 | setActiveExample(_ => SubscribeToMore)}>
45 | {React.string("Subscribe to More")}
46 |
47 | setActiveExample(_ => LoadMoreFragments)}>
50 | {React.string("Load More Fragments")}
51 |
52 |
53 |
54 | {switch (activeExample) {
55 | | LoadMore =>
56 | <>
57 |
58 |
59 |
63 |
{React.string("FilterByAge.re")}
64 |
65 |
66 |
67 | {React.string("FilterByAgeErrorHandling.re")}
68 |
69 |
70 |
71 | {React.string("FilterByAgeFragment.re")}
72 |
73 | {React.string("FilterByNameCache.re")}
74 |
75 |
76 |
77 |
78 | >
79 | | LoadMoreFragments =>
80 | <>
81 |
82 |
83 |
87 |
{React.string("FilterByAge.re")}
88 |
89 |
90 |
91 | {React.string("FilterByAgeErrorHandling.re")}
92 |
93 |
94 |
95 | {React.string("FilterByAgeFragment.re")}
96 |
97 | {React.string("FilterByNameCache.re")}
98 |
99 |
100 |
101 |
102 | >
103 | | SubscribeToMore =>
104 | <>
105 |
108 |
109 | >
110 | }}
111 |
112 | >;
113 | };
--------------------------------------------------------------------------------
/examples/persons/src/SubscribeToMore.re:
--------------------------------------------------------------------------------
1 | module GetAllPersons = [%graphql
2 | {|
3 | query getAllPersons {
4 | allPersons {
5 | id
6 | name
7 | }
8 | }
9 | |}
10 | ];
11 |
12 | // NOTE: The shape of the returned data should exactly match the shape of the GetAllPersons query
13 | // See https://www.apollographql.com/docs/react/data/subscriptions/#subscribetomore for more information
14 | module NewPerson = [%graphql
15 | {|
16 | subscription {
17 | Person {
18 | node {
19 | id
20 | name
21 | }
22 | }
23 | }
24 | |}
25 | ];
26 |
27 | // Defining those types and "%identity" converters below allows us to
28 | // write the updateQuery in pure Reason and avoid bs.raw alltogether
29 | type person = {
30 | id: string,
31 | name: string,
32 | };
33 |
34 | type allPersons = {
35 | __typename: string,
36 | allPersons: array(person),
37 | };
38 |
39 | type subscriptionNode = {node: person};
40 |
41 | [@bs.deriving abstract]
42 | type newPerson = {
43 | [@bs.as "Person"]
44 | person: subscriptionNode,
45 | };
46 |
47 | external resultToJson: allPersons => Js.Json.t = "%identity";
48 | external toPrevResult: Js.Json.t => Js.Nullable.t(allPersons) = "%identity";
49 | external toSubscriptionData: Js.Json.t => Js.Nullable.t(newPerson) =
50 | "%identity";
51 |
52 | [@react.component]
53 | let make = () => {
54 | let newPerson = NewPerson.make();
55 | let newPersonDocument = ApolloClient.gql(. newPerson##query);
56 |
57 | let (simple, full) = ApolloHooks.useQuery(GetAllPersons.definition);
58 |
59 | let subscribe = full.subscribeToMore;
60 | React.useEffect1(
61 | () => {
62 | let unsubscribe =
63 | subscribe(
64 | ~document=newPersonDocument,
65 | ~updateQuery=
66 | (maybePrevResult, maybeSubscriptionPayload) =>
67 | switch (
68 | maybePrevResult |> toPrevResult |> Js.Nullable.toOption,
69 | maybeSubscriptionPayload##subscriptionData##data
70 | |> toSubscriptionData
71 | |> Js.Nullable.toOption,
72 | ) {
73 | | (Some(prev), Some(newData)) =>
74 | {
75 | ...prev, // NOTE: This only works with BuckleScript 7
76 | allPersons:
77 | prev.allPersons
78 | ->Belt.Array.concat([|newData->personGet.node|]),
79 | }
80 | ->resultToJson
81 | | _ => maybePrevResult
82 | },
83 | (),
84 | );
85 |
86 | Some(unsubscribe);
87 | },
88 | [|subscribe|],
89 | );
90 |
91 |
92 | {switch (simple) {
93 | | Loading =>
{React.string("Loading...")}
94 | | Data(data) =>
95 | data##allPersons
96 | ->Belt.Array.reverse
97 | ->Belt.Array.map(person =>
98 |
99 |
100 | {React.string("Id: ")}
101 | {React.string(person##id)}
102 |
103 |
104 |
105 | {React.string("Name: ")}
106 |
107 | {React.string(person##name)}
108 |
109 |
110 | )
111 | |> React.array
112 | | Error(_) =>
{React.string("Error")}
113 | | NoData =>
{React.string("Not asked")}
114 | }}
115 |
;
116 | };
117 |
--------------------------------------------------------------------------------
/examples/persons/src/fragments/FilterByAgeFragment.re:
--------------------------------------------------------------------------------
1 | open Fragments;
2 | open ApolloHooks;
3 |
4 | module PersonsOlderThanQuery = [%graphql
5 | {|
6 | query getPersonsOlderThan($age: Int!) {
7 | allPersons(filter: { age_gte: $age } ) {
8 | ...PersonFragment.Person
9 | }
10 | }
11 | |}
12 | ];
13 |
14 | [@react.component]
15 | let make = (~age) => {
16 | let (simple, _full) =
17 | useQuery(
18 | ~variables=PersonsOlderThanQuery.makeVariables(~age, ()),
19 | PersonsOlderThanQuery.definition,
20 | );
21 |
22 |
23 | {switch (simple) {
24 | | Loading =>
{React.string("Loading...")}
25 | | Data(data) =>
26 |
27 | {"There are "
28 | ++ (data##allPersons->Belt.Array.length |> string_of_int)
29 | ++ " people older than "
30 | ++ string_of_int(age)
31 | |> React.string}
32 |
33 | | NoData
34 | | Error(_) =>
{React.string("Error")}
35 | }}
36 |
;
37 | };
38 |
--------------------------------------------------------------------------------
/examples/persons/src/fragments/Fragments.re:
--------------------------------------------------------------------------------
1 | // example of creating a fragment on a type available from your grapqlql-ppx types.
2 |
3 | // In this contrived example, here instead of using the following as seen in the `FilterByAge.re`
4 | // module PersonsOlderThanQuery = [%graphql
5 | // {|
6 | // query getPersonsOlderThan($age: Int!) {
7 | // allPersons(filter: { age_gte: $age } ) {
8 | // id
9 | // }
10 | // }
11 | // |}
12 | // ];
13 |
14 | // we can use the following as seen in `FilterByAgeFragment.re`
15 | // ```
16 | // module PersonsOlderThanQuery = [%graphql
17 | // {|
18 | // query getPersonsOlderThan($age: Int!) {
19 | // allPersons(filter: { age_gte: $age } ) {
20 | // ...PersonFragment.Person
21 | // }
22 | // }
23 | // |}
24 | // ];
25 | // ```
26 | module PersonFragment = [%graphql
27 | {|
28 | fragment person on Person {
29 | id
30 | name
31 | age
32 | }
33 | |}
34 | ];
35 | module PersonIdFragment = [%graphql
36 | {|
37 | fragment person on Person {
38 | id
39 | }
40 | |}
41 | ];
42 | module PersonAgeFragment = [%graphql
43 | {|
44 | fragment person on Person {
45 | age
46 | }
47 | |}
48 | ];
--------------------------------------------------------------------------------
/examples/persons/src/fragments/LoadMoreFragments.re:
--------------------------------------------------------------------------------
1 | module GetAllPersonsQuery = [%graphql
2 | {|
3 | query getAllPersons($skip: Int!, $first: Int!) {
4 | allPersons(skip: $skip, first: $first) {
5 | ...Fragments.PersonFragment.Person
6 | }
7 | }
8 | |}
9 | ];
10 |
11 | let personsPerPage = 5;
12 |
13 | [@react.component]
14 | let make = () => {
15 | let (_simple, full) =
16 | ApolloHooks.useQuery(
17 | ~variables=
18 | GetAllPersonsQuery.makeVariables(~skip=0, ~first=personsPerPage, ()),
19 | ~notifyOnNetworkStatusChange=true,
20 | GetAllPersonsQuery.definition,
21 | );
22 |
23 | let handleLoadMore = _ => {
24 | let skip =
25 | switch (full) {
26 | | {data: Some(data)} => data##allPersons->Belt.Array.length
27 | | _ => 0
28 | };
29 |
30 | full.fetchMore(
31 | ~variables=
32 | GetAllPersonsQuery.makeVariables(~skip, ~first=personsPerPage, ()),
33 | ~updateQuery=[%bs.raw
34 | {|
35 | function(prevResult, { fetchMoreResult, ...rest }) {
36 | if (!fetchMoreResult) return prevResult;
37 | return {
38 | ...fetchMoreResult,
39 | allPersons: prevResult.allPersons.concat(fetchMoreResult.allPersons)
40 | };
41 | }
42 | |}
43 | ],
44 | (),
45 | )
46 | |> ignore;
47 | };
48 |
49 |
50 | {switch (full) {
51 | | {loading: true, data: None} =>
{React.string("Loading...")}
52 | | {data: Some(data)} =>
53 | <>
54 |
55 |
57 | {React.string("Load more")}
58 |
59 | >
60 | | {error: Some(_)} =>
{React.string("Error")}
61 | | {error: None, data: None, loading: false} =>
62 |
{React.string("Not asked")}
63 | }}
64 |
;
65 | };
66 |
--------------------------------------------------------------------------------
/examples/persons/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reason Apollo Hooks example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/persons/src/styles.css:
--------------------------------------------------------------------------------
1 | #root {
2 | padding: 10px 40px;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .tab-content {
8 | padding: 10px 40px;
9 | display: flex;
10 | border: 1px solid black;
11 | }
12 |
13 | input {
14 | font-size: 14px;
15 | padding: 3px 5px;
16 | }
17 |
18 | .form-field {
19 | margin-bottom: 15px;
20 | }
21 |
22 | .person-list {
23 | width: 350px;
24 | padding-top: 25px;
25 | margin-left: 100px;
26 | }
27 |
28 | .person {
29 | margin-bottom: 15px;
30 | border-bottom: 1px solid #aaa;
31 | }
32 |
33 | .person-field {
34 | margin-bottom: 10px;
35 | }
36 |
37 | .person-label {
38 | font-weight: bold;
39 | }
40 |
41 | .edit-person,
42 | .add-person {
43 | position: fixed;
44 | }
45 |
46 | .edit-person-container,
47 | .add-person-container {
48 | width: 300px;
49 | }
50 |
51 | .tab {
52 | font-size: x-large;
53 | border-width: 2px 2px 0px 2px;
54 | border-color: black;
55 | margin: 2px 2px 0px 2px;
56 | padding: 5px 10px 10px 5px;
57 | background-color: lightgrey;
58 | }
59 |
60 | .selected-tab {
61 | background-color: white;
62 | }
63 |
--------------------------------------------------------------------------------
/examples/persons/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const HtmlWebpackPlugin = require("html-webpack-plugin")
3 | const outputDir = path.join(__dirname, "build/")
4 |
5 | const isProd = process.env.NODE_ENV === "production"
6 |
7 | module.exports = {
8 | entry: "./src/Index.bs.js",
9 | mode: isProd ? "production" : "development",
10 | output: {
11 | path: outputDir,
12 | filename: "Index.js"
13 | },
14 | plugins: [
15 | new HtmlWebpackPlugin({
16 | template: "src/index.html",
17 | inject: false
18 | })
19 | ],
20 | devServer: {
21 | compress: true,
22 | contentBase: outputDir,
23 | port: process.env.PORT || 8000,
24 | historyApiFallback: true
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.css$/,
30 | use: ["style-loader", "css-loader"]
31 | }
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reason-apollo-hooks",
3 | "version": "6.0.1",
4 | "scripts": {
5 | "build": "bsb -make-world",
6 | "start": "bsb -make-world -w",
7 | "clean": "bsb -clean-world",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "build:docs": "yarn build && bsdoc build --debug --verbose ReasonApolloHooks"
10 | },
11 | "keywords": [
12 | "BuckleScript"
13 | ],
14 | "author": "",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@apollo/react-hooks": "^3.0.0",
18 | "@commitlint/cli": "^8.2.0",
19 | "@commitlint/config-conventional": "^8.2.0",
20 | "bs-platform": "^7.0.1",
21 | "bsdoc": "6.0.1-alpha",
22 | "husky": "^3.0.5",
23 | "lint-staged": "^9.4.0",
24 | "reason-apollo": "^0.20.0",
25 | "reason-react": ">=0.7.0"
26 | },
27 | "peerDependencies": {
28 | "@apollo/react-hooks": "^3.0.0",
29 | "react": "^16.8.6",
30 | "react-dom": "^16.8.6",
31 | "reason-apollo": "^0.20.0",
32 | "reason-react": ">=0.7.0"
33 | },
34 | "lint-staged": {
35 | "*.{re,rei}": [
36 | "bsrefmt --in-place -w 80",
37 | "git add"
38 | ]
39 | },
40 | "husky": {
41 | "hooks": {
42 | "pre-commit": "lint-staged",
43 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/ApolloHooks.re:
--------------------------------------------------------------------------------
1 | module Mutation = ApolloHooksMutation;
2 | module Query = ApolloHooksQuery;
3 | module Provider = ApolloHooksProvider;
4 | module Subscription = ApolloHooksSubscription;
5 |
6 | /**
7 | This is probably the one hook you'll use the most. A quick demo:
8 |
9 | {[
10 | open ApolloHooks;
11 |
12 | module Query = [%graphql {|
13 | query MyQuery {
14 | me { id, name }
15 | }
16 | |}];
17 |
18 | [@react.component]
19 | let make = () => {
20 | /* In Reason we prefix variables that we are not going to use with _ */
21 | let (simple, _full) = useQuery(Query.definitions);
22 |
23 | /* When using simple with Reason's pattern-matching operator, the compiler will force you to cover every single branch of the variant type */
24 | switch(simple) {
25 | | Loading => React.string("loading...")
26 | | Error(error) =>
27 | Js.log(error);
28 | React.string("Something went wrong!")
29 | | Data(data) =>
30 | React.string("Hello, " ++ data##me##name)
31 | /* Every. Single. One. Of Them. */
32 | | NoData =>
33 | React.string("Woa something went really wrong! Glady we use Reason and it forced us to handle this! Report this issue")
34 | }
35 | }
36 | ]}
37 |
38 | Why we return a tuple? While designing and using the API we came to the conclusion that would be much more convient to have a value that would attend
39 | the majority of simple usages and a full for when you need to do a complex UI, such as infinite scroll.
40 |
41 | The value [simple] ({!type:Query.variant('a)}) helps you to consume your data with simplicity, type safety and exhaustiveness check.
42 | But for those cases where you really want do do a fine-grained control of your data flow – such as when you have [loading] and [data] at the same time –
43 | that's when [full] ({!type:Query.queryResult('a)}) becomes more useful.
44 |
45 | {[
46 | module Query = [%graphql {|
47 | query MyQuery {
48 | me { id, name }
49 | }
50 | |}];
51 |
52 | [@react.component]
53 | let make = () => {
54 | let (_simple, full) = useQuery(Query.definitions);
55 |
56 | /* `full` is a record type so you pattern against it's possible combos of values */
57 | switch(full) {
58 | /* Initial loading */
59 | | { loading: true, data: None } => React.string("loading...")
60 | /* Error but no data */
61 | | { loading: false, data: None, error: Some(error) } => React.string("Something went wrong")
62 | /* When we have some data and we tried to refetch but got an error */
63 | | { loading: false, data: Some(data), error: Some(error) } =>
64 | <>
65 | {React.string("Something went wrong")}
66 |
67 | >
68 | /* Just data */
69 | | { loading: false, data: Some(data), error: None } =>
70 | <>
71 | {React.string("Something went wrong")}
72 |
73 | >
74 | | Data(data) =>
75 | React.string("Hello, " ++ data##me##name)
76 | /* Not loading? No data? No error? That's weird */
77 | | {loading: false, data: None, error: null} =>
78 | React.string("Woa something went really wrong! But the programmer remembered to handle this case! Report to us")
79 | }
80 | }
81 | ]}
82 |
83 | Quite more complex right? Gladly it's not always that we have that level of complexity.
84 |
85 | That covers the most common cases of usage. If you want to see more complex usages check out the examples folder.
86 | */
87 | let useQuery = Query.useQuery;
88 |
89 | /**
90 | Second most used! Here's a quick demo:
91 |
92 | {[
93 | open ApolloHooks;
94 |
95 | module Mutation = [%graphql {|
96 | mutation MyMutation($input: MyMutationInput!) {
97 | myMutation(input: $input) { error }
98 | }
99 | |}];
100 |
101 | [@react.component]
102 | let make = () => {
103 | /* `simple` and `full` follow the same principle of `useQuery`. */
104 | let (mutate, simple, _full) = useMutation(Mutation.definitions);
105 |
106 | /* When using simple with Reason's pattern-matching operator, the compiler will force you to cover every single branch of the variant type */
107 | switch(simple) {
108 | | Loading => React.string("loading...")
109 | | Error(error) =>
110 | Js.log(error);
111 | React.string("Something went wrong!")
112 | | Data(data) =>
113 |
114 | {React.string("Hello, " ++ data##me##name)}
115 |
116 | /* Every. Single. One. Of Them. */
117 | | NotCalled => mutate()}>{React.string("Click me")}
118 | | NoData =>
119 | React.string("Woa something went really wrong! Glady we use Reason and it forced us to handle this! Report this issue")
120 | }
121 | }
122 | ]}
123 |
124 | Or if you only care about calling [mutate]
125 |
126 | {[
127 | open ApolloHooks;
128 |
129 | module Mutation = [%graphql {|
130 | mutation MyMutation {
131 | me { id, name }
132 | }
133 | |}];
134 |
135 | [@react.component]
136 | let make = () => {
137 | let (mutate, _simple, _full) = useMutation(Mutation.definitions);
138 | let onClick = _event => {
139 | mutate()
140 | |> Js.Promise.then_(result => {
141 | switch(result) {
142 | | Data(data) => do anything here
143 | | Error(error) => handle your error
144 | | NoData => ...something went wrong...
145 | }
146 | })
147 | }
148 |
149 | {React.string("Click me")}
150 | }
151 | ]}
152 | */
153 | let useMutation = Mutation.useMutation;
154 |
155 | /** useSubscription bindings */
156 | let useSubscription = Subscription.useSubscription;
157 |
158 | /** Helper to generate the shape of a query for [refetchQueries] mutation param. Take a look in examples/persons/src/EditPerson.re for a more complete demo of usage. */
159 | let toQueryObj = result =>
160 | ApolloClient.{
161 | query: ApolloClient.gql(. result##query),
162 | variables: result##variables,
163 | };
164 |
--------------------------------------------------------------------------------
/src/ApolloHooksMutation.re:
--------------------------------------------------------------------------------
1 | open ApolloHooksTypes;
2 |
3 | type jsResult = {
4 | .
5 | "data": Js.Nullable.t(Js.Json.t),
6 | "loading": bool,
7 | "called": bool,
8 | "error": Js.Nullable.t(apolloError),
9 | };
10 |
11 | type jsExecutionResult = {
12 | .
13 | "data": Js.Nullable.t(Js.Json.t),
14 | "errors": Js.Nullable.t(array(graphqlError)),
15 | };
16 |
17 | type refetchQueries = jsExecutionResult => array(ApolloClient.queryObj);
18 |
19 | /* The type that the promise returned by the mutate function resolves to */
20 | type executionResult('a) = {
21 | data: option('a),
22 | errors: option(array(graphqlError)),
23 | };
24 |
25 | type executionVariantResult('a) =
26 | | Data('a)
27 | | Errors(array(graphqlError))
28 | | NoData;
29 |
30 | /* The type of the 'full' result returned by the hook */
31 | type controlledResult('a) = {
32 | loading: bool,
33 | called: bool,
34 | data: option('a),
35 | error: option(apolloError),
36 | };
37 |
38 | /* The type of the 'simple' result returned by the hook */
39 | type controlledVariantResult('a) =
40 | | Loading
41 | | NotCalled
42 | | Data('a)
43 | | Error(apolloError)
44 | | NoData;
45 |
46 | [@bs.module "graphql-tag"] external gql: ReasonApolloTypes.gql = "default";
47 |
48 | type mutationResult('a) = {. "data": option('a)};
49 |
50 | [@bs.deriving abstract]
51 | type options('a) = {
52 | [@bs.optional]
53 | variables: Js.Json.t,
54 | [@bs.optional]
55 | mutation: option(ReasonApolloTypes.queryString),
56 | [@bs.optional]
57 | client: ApolloClient.generatedApolloClient,
58 | [@bs.optional]
59 | refetchQueries,
60 | [@bs.optional]
61 | awaitRefetchQueries: bool,
62 | [@bs.optional]
63 | update: (ApolloClient.generatedApolloClient, mutationResult('a)) => unit,
64 | [@bs.optional]
65 | optimisticResponse: Js.Json.t,
66 | [@bs.optional]
67 | errorPolicy: string,
68 | [@bs.optional]
69 | context: Context.t,
70 | };
71 |
72 | type jsMutate('a) = (. options('a)) => Js.Promise.t(jsExecutionResult);
73 |
74 | type mutation('a) =
75 | (
76 | ~variables: Js.Json.t=?,
77 | ~client: ApolloClient.generatedApolloClient=?,
78 | ~refetchQueries: refetchQueries=?,
79 | ~awaitRefetchQueries: bool=?,
80 | ~optimisticResponse: Js.Json.t=?,
81 | unit
82 | ) =>
83 | Js.Promise.t((executionVariantResult('a), executionResult('a)));
84 |
85 | [@bs.module "@apollo/react-hooks"]
86 | external useMutationJs:
87 | (. ReasonApolloTypes.queryString, options('a)) => (jsMutate('a), jsResult) =
88 | "useMutation";
89 |
90 | exception Error(string);
91 |
92 | let useMutation:
93 | (
94 | ~client: ApolloClient.generatedApolloClient=?,
95 | ~variables: Js.Json.t=?,
96 | ~refetchQueries: refetchQueries=?,
97 | ~awaitRefetchQueries: bool=?,
98 | ~update: (ApolloClient.generatedApolloClient, mutationResult('data)) =>
99 | unit
100 | =?,
101 | ~optimisticResponse: Js.Json.t=?,
102 | ~errorPolicy: ApolloHooksTypes.errorPolicy=?,
103 | ~context: Context.t=?,
104 | ApolloHooksTypes.graphqlDefinition('data, _, _)
105 | ) =>
106 | (
107 | mutation('data),
108 | controlledVariantResult('data),
109 | controlledResult('data),
110 | ) =
111 | (
112 | ~client=?,
113 | ~variables=?,
114 | ~refetchQueries=?,
115 | ~awaitRefetchQueries=?,
116 | ~update=?,
117 | ~optimisticResponse=?,
118 | ~errorPolicy=?,
119 | ~context=?,
120 | (parse, query, _),
121 | ) => {
122 | let (jsMutate, jsResult) =
123 | useMutationJs(.
124 | gql(. query),
125 | options(
126 | ~client?,
127 | ~variables?,
128 | ~refetchQueries?,
129 | ~awaitRefetchQueries?,
130 | ~update?,
131 | ~optimisticResponse?,
132 | ~errorPolicy=?
133 | errorPolicy->Belt.Option.map(ApolloHooksTypes.errorPolicyToJs),
134 | ~context?,
135 | (),
136 | ),
137 | );
138 |
139 | let mutate =
140 | React.useMemo1(
141 | (
142 | (),
143 | ~variables=?,
144 | ~client=?,
145 | ~refetchQueries=?,
146 | ~awaitRefetchQueries=?,
147 | ~optimisticResponse=?,
148 | (),
149 | ) =>
150 | jsMutate(.
151 | options(
152 | ~variables?,
153 | ~client?,
154 | ~refetchQueries?,
155 | ~awaitRefetchQueries?,
156 | ~optimisticResponse?,
157 | (),
158 | ),
159 | )
160 | |> Js.Promise.then_(jsResult => {
161 | let full = {
162 | data:
163 | Js.Nullable.toOption(jsResult##data)
164 | ->Belt.Option.map(parse),
165 | errors:
166 | switch (Js.Nullable.toOption(jsResult##errors)) {
167 | | Some(errors) when Js.Array.length(errors) > 0 =>
168 | Some(errors)
169 | | _ => None
170 | },
171 | };
172 |
173 | let simple =
174 | switch (full) {
175 | | {errors: Some(errors)} => (
176 | Errors(errors): executionVariantResult('data)
177 | )
178 | | {data: Some(data)} => Data(data)
179 | | {errors: None, data: None} => NoData
180 | };
181 |
182 | (simple, full) |> Js.Promise.resolve;
183 | }),
184 | [|variables|],
185 | );
186 |
187 | let full =
188 | React.useMemo1(
189 | () =>
190 | {
191 | loading: jsResult##loading,
192 | called: jsResult##called,
193 | data:
194 | jsResult##data->Js.Nullable.toOption->Belt.Option.map(parse),
195 | error: jsResult##error->Js.Nullable.toOption,
196 | },
197 | [|jsResult|],
198 | );
199 |
200 | let simple =
201 | React.useMemo1(
202 | () =>
203 | switch (full) {
204 | | {loading: true} => Loading
205 | | {error: Some(error)} => Error(error)
206 | | {data: Some(data)} => Data(data)
207 | | {called: false} => NotCalled
208 | | _ => NoData
209 | },
210 | [|full|],
211 | );
212 |
213 | (mutate, simple, full);
214 | };
215 |
--------------------------------------------------------------------------------
/src/ApolloHooksProvider.re:
--------------------------------------------------------------------------------
1 | [@bs.module "@apollo/react-hooks"] [@react.component]
2 | external make:
3 | (~client: ApolloClient.generatedApolloClient, ~children: React.element) =>
4 | React.element =
5 | "ApolloProvider";
--------------------------------------------------------------------------------
/src/ApolloHooksQuery.re:
--------------------------------------------------------------------------------
1 | open ApolloHooksTypes;
2 |
3 | type variant('a) =
4 | | Data('a)
5 | | Error(apolloError)
6 | | Loading
7 | | NoData;
8 |
9 | /**
10 | *
11 | * apollo-client/src/core/ObservableQuery.ts
12 | */
13 | [@bs.deriving abstract]
14 | type updateQueryOptions = {
15 | [@bs.optional]
16 | fetchMoreResult: Js.Json.t,
17 | [@bs.optional]
18 | variables: Js.Json.t,
19 | };
20 |
21 | type updateQueryT = (Js.Json.t, updateQueryOptions) => Js.Json.t;
22 |
23 | /**
24 | * https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/watchQueryOptions.ts#L139
25 | */
26 | type updateSubscriptionOptionsJs = {
27 | .
28 | "subscriptionData": {. "data": Js.Json.t},
29 | "variables": Js.Nullable.t(Js.Json.t),
30 | };
31 |
32 | type updateQuerySubscribeToMoreT =
33 | (Js.Json.t, updateSubscriptionOptionsJs) => Js.Json.t;
34 |
35 | [@bs.deriving abstract]
36 | type subscribeToMoreOptionsJs = {
37 | document: ReasonApolloTypes.queryString,
38 | [@bs.optional]
39 | variables: Js.Json.t,
40 | [@bs.optional]
41 | updateQuery: updateQuerySubscribeToMoreT,
42 | };
43 |
44 | type unsubscribeFnT = unit => unit;
45 |
46 | type refetch('a) = (~variables: Js.Json.t=?, unit) => Js.Promise.t('a);
47 | type queryResult('a) = {
48 | data: option('a),
49 | loading: bool,
50 | error: option(apolloError),
51 | refetch: refetch('a),
52 | fetchMore:
53 | (~variables: Js.Json.t=?, ~updateQuery: updateQueryT, unit) =>
54 | Js.Promise.t(unit),
55 | networkStatus: ApolloHooksTypes.networkStatus,
56 | startPolling: int => unit,
57 | stopPolling: unit => unit,
58 | subscribeToMore:
59 | (
60 | ~document: ReasonApolloTypes.queryString,
61 | ~variables: Js.Json.t=?,
62 | ~updateQuery: updateQuerySubscribeToMoreT=?,
63 | unit
64 | ) =>
65 | unsubscribeFnT,
66 | };
67 |
68 | /**
69 | * apollo-client/src/core/watchQueryOptions.ts
70 | */
71 | [@bs.deriving abstract]
72 | type fetchMoreOptions = {
73 | [@bs.optional]
74 | variables: Js.Json.t,
75 | updateQuery: updateQueryT,
76 | };
77 |
78 | [@bs.module "graphql-tag"] external gql: ReasonApolloTypes.gql = "default";
79 |
80 | [@bs.deriving abstract]
81 | type options = {
82 | [@bs.optional]
83 | variables: Js.Json.t,
84 | [@bs.optional]
85 | client: ApolloClient.generatedApolloClient,
86 | [@bs.optional]
87 | notifyOnNetworkStatusChange: bool,
88 | [@bs.optional]
89 | fetchPolicy: string,
90 | [@bs.optional]
91 | errorPolicy: string,
92 | [@bs.optional]
93 | skip: bool,
94 | [@bs.optional]
95 | pollInterval: int,
96 | [@bs.optional]
97 | context: Context.t,
98 | };
99 |
100 | [@bs.module "@apollo/react-hooks"]
101 | external useQueryJs:
102 | (ReasonApolloTypes.queryString, options) =>
103 | {
104 | .
105 | "data": Js.Nullable.t(Js.Json.t),
106 | "loading": bool,
107 | "error": Js.Nullable.t(apolloError),
108 | [@bs.meth]
109 | "refetch": Js.Nullable.t(Js.Json.t) => Js.Promise.t(Js.Json.t),
110 | [@bs.meth] "fetchMore": fetchMoreOptions => Js.Promise.t(unit),
111 | "networkStatus": Js.Nullable.t(int),
112 | [@bs.meth] "stopPolling": unit => unit,
113 | [@bs.meth] "startPolling": int => unit,
114 | [@bs.meth] "subscribeToMore": subscribeToMoreOptionsJs => unsubscribeFnT,
115 | } =
116 | "useQuery";
117 |
118 | let useQuery:
119 | (
120 | ~client: ApolloClient.generatedApolloClient=?,
121 | ~variables: Js.Json.t=?,
122 | ~notifyOnNetworkStatusChange: bool=?,
123 | ~fetchPolicy: ApolloHooksTypes.fetchPolicy=?,
124 | ~errorPolicy: ApolloHooksTypes.errorPolicy=?,
125 | ~skip: bool=?,
126 | ~pollInterval: int=?,
127 | ~context: Context.t=?,
128 | graphqlDefinition('data, _, _)
129 | ) =>
130 | (variant('data), queryResult('data)) =
131 | (
132 | ~client=?,
133 | ~variables=?,
134 | ~notifyOnNetworkStatusChange=?,
135 | ~fetchPolicy=?,
136 | ~errorPolicy=?,
137 | ~skip=?,
138 | ~pollInterval=?,
139 | ~context=?,
140 | (parse, query, _),
141 | ) => {
142 | let jsResult =
143 | useQueryJs(
144 | gql(. query),
145 | options(
146 | ~variables?,
147 | ~client?,
148 | ~notifyOnNetworkStatusChange?,
149 | ~fetchPolicy=?
150 | fetchPolicy->Belt.Option.map(ApolloHooksTypes.fetchPolicyToJs),
151 | ~errorPolicy=?
152 | errorPolicy->Belt.Option.map(ApolloHooksTypes.errorPolicyToJs),
153 | ~skip?,
154 | ~pollInterval?,
155 | ~context?,
156 | (),
157 | ),
158 | );
159 |
160 | let getData = obj =>
161 | obj
162 | ->Js.Json.decodeObject
163 | ->Belt.Option.flatMap(x => Js.Dict.get(x, "data"))
164 | ->Belt.Option.getExn;
165 |
166 | let result =
167 | React.useMemo1(
168 | () =>
169 | {
170 | data:
171 | jsResult##data
172 | ->Js.Nullable.toOption
173 | ->Belt.Option.flatMap(data =>
174 | switch (parse(data)) {
175 | | parsedData => Some(parsedData)
176 | | exception _ => None
177 | }
178 | ),
179 | loading: jsResult##loading,
180 | error: jsResult##error->Js.Nullable.toOption,
181 | networkStatus:
182 | ApolloHooksTypes.toNetworkStatus(jsResult##networkStatus),
183 | refetch: (~variables=?, ()) =>
184 | jsResult##refetch(Js.Nullable.fromOption(variables))
185 | |> Js.Promise.then_(result =>
186 | parse(result->getData) |> Js.Promise.resolve
187 | ),
188 | fetchMore: (~variables=?, ~updateQuery, ()) =>
189 | jsResult##fetchMore(
190 | fetchMoreOptions(~variables?, ~updateQuery, ()),
191 | ),
192 | stopPolling: () => jsResult##stopPolling(),
193 | startPolling: interval => jsResult##startPolling(interval),
194 | subscribeToMore: (~document, ~variables=?, ~updateQuery=?, ()) =>
195 | jsResult##subscribeToMore(
196 | subscribeToMoreOptionsJs(
197 | ~document,
198 | ~variables?,
199 | ~updateQuery?,
200 | (),
201 | ),
202 | ),
203 | },
204 | [|jsResult|],
205 | );
206 |
207 | let simple =
208 | React.useMemo1(
209 | () =>
210 | switch (result) {
211 | | {loading: true} => Loading
212 | | {error: Some(error)} => Error(error)
213 | | {data: Some(data)} => Data(data)
214 | | _ => NoData
215 | },
216 | [|result|],
217 | );
218 |
219 | (simple, result);
220 | };
221 |
--------------------------------------------------------------------------------
/src/ApolloHooksSubscription.re:
--------------------------------------------------------------------------------
1 | type error = {. "message": string};
2 |
3 | type variant('a) =
4 | | Data('a)
5 | | Error(error)
6 | | Loading
7 | | NoData;
8 |
9 | type result('a) = {
10 | data: option('a),
11 | loading: bool,
12 | error: option(error),
13 | };
14 |
15 | [@bs.module "graphql-tag"] external gql: ReasonApolloTypes.gql = "default";
16 |
17 | [@bs.deriving abstract]
18 | type options = {
19 | [@bs.optional]
20 | variables: Js.Json.t,
21 | [@bs.optional]
22 | skip: bool,
23 | [@bs.optional]
24 | onSubscriptionData: unit => unit,
25 | [@bs.optional]
26 | client: ApolloClient.generatedApolloClient,
27 | };
28 |
29 | [@bs.module "@apollo/react-hooks"]
30 | external useSubscriptionJs:
31 | (ReasonApolloTypes.queryString, options) =>
32 | {
33 | .
34 | "data": Js.Nullable.t(Js.Json.t),
35 | "loading": bool,
36 | "error": Js.Nullable.t(error),
37 | } =
38 | "useSubscription";
39 |
40 | let useSubscription:
41 | (
42 | ~variables: Js.Json.t=?,
43 | ~client: ApolloClient.generatedApolloClient=?,
44 | ~skip: bool=?,
45 | ApolloHooksTypes.graphqlDefinition('data, _, _)
46 | ) =>
47 | (variant('data), result('data)) =
48 | (~variables=?, ~client=?, ~skip=?, (parse, query, _)) => {
49 | let jsResult =
50 | useSubscriptionJs(
51 | gql(. query),
52 | options(~variables?, ~client?, ~skip?, ()),
53 | );
54 |
55 | let result = {
56 | data: jsResult##data->Js.Nullable.toOption->Belt.Option.map(parse),
57 | loading: jsResult##loading,
58 | error: jsResult##error->Js.Nullable.toOption,
59 | };
60 |
61 | (
62 | switch (result) {
63 | | {data: Some(data)} => Data(data)
64 | | {error: Some(error)} => Error(error)
65 | | {loading: true} => Loading
66 | | _ => NoData
67 | },
68 | result,
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/src/ApolloHooksTypes.re:
--------------------------------------------------------------------------------
1 | /**
2 | * apollo-client/src/core/networkStatus
3 | */
4 | type networkStatus =
5 | | Loading
6 | | SetVariables
7 | | FetchMore
8 | | Refetch
9 | | Poll
10 | | Ready
11 | | Error
12 | | Unknown;
13 |
14 | let toNetworkStatus = (status: Js.Nullable.t(int)) => {
15 | switch (status->Js.Nullable.toOption) {
16 | | Some(1) => Loading
17 | | Some(2) => SetVariables
18 | | Some(3) => FetchMore
19 | | Some(4) => Refetch
20 | | Some(6) => Poll
21 | | Some(7) => Ready
22 | | Some(8) => Error
23 | | _ => Unknown
24 | };
25 | };
26 |
27 | /**
28 | * apollo-client/src/core/watchQueryOptions.ts
29 | */
30 | type fetchPolicy =
31 | | CacheFirst
32 | | CacheAndNetwork
33 | | NetworkOnly
34 | | CacheOnly
35 | | NoCache
36 | | Standby;
37 |
38 | let fetchPolicyToJs = fetchPolicy => {
39 | switch (fetchPolicy) {
40 | | CacheFirst => "cache-first"
41 | | CacheAndNetwork => "cache-and-network"
42 | | NetworkOnly => "network-only"
43 | | CacheOnly => "cache-only"
44 | | NoCache => "no-cache"
45 | | Standby => "standby"
46 | };
47 | };
48 |
49 | /**
50 | * apollo-client/src/core/watchQueryOptions.ts
51 | */
52 | type errorPolicy =
53 | | None
54 | | Ignore
55 | | All;
56 |
57 | let errorPolicyToJs = errorPolicy =>
58 | switch (errorPolicy) {
59 | | None => "none"
60 | | Ignore => "ignore"
61 | | All => "all"
62 | };
63 |
64 | /**
65 | * apollo-client/src/errors/ApolloError.ts
66 | */
67 | type apolloErrorExtensions = {. "code": Js.Nullable.t(string)};
68 |
69 | type graphqlError = {
70 | .
71 | "message": string,
72 | "name": Js.Nullable.t(string),
73 | "extensions": Js.Nullable.t(apolloErrorExtensions),
74 | "locations": Js.Nullable.t(array(string)),
75 | "path": Js.Nullable.t(array(string)),
76 | "nodes": Js.Nullable.t(array(string)),
77 | };
78 |
79 | type apolloError = {
80 | .
81 | "message": string,
82 | "graphQLErrors": Js.Nullable.t(array(graphqlError)),
83 | "networkError": Js.Nullable.t(string),
84 | };
85 |
86 | type parse('a) = Js.Json.t => 'a;
87 | type query = string;
88 | type composeVariables('returnType, 'hookReturnType) =
89 | (Js.Json.t => 'returnType) => 'hookReturnType;
90 |
91 | type graphqlDefinition('data, 'returnType, 'hookReturnType) = (
92 | parse('data),
93 | query,
94 | composeVariables('returnType, 'hookReturnType),
95 | );
96 |
97 | module Context = {
98 | type t = Js.Dict.t(string);
99 | let make = (context): t => Js.Dict.fromList(context);
100 | };
101 |
--------------------------------------------------------------------------------
/src/index.mld:
--------------------------------------------------------------------------------
1 | {2:top Index}
2 |
3 | Hello!
4 |
5 | Check out the module {!module:ApolloHooks} to get started.
6 |
--------------------------------------------------------------------------------