Conversation
kiraarghy
commented
Jun 5, 2019
- Created UrqlUseSubscription.
src/components/UrqlSubscription.re
Outdated
| ~children: subscriptionRenderProps('a) => React.element, | ||
| ) => | ||
| switch (variables, handler) { | ||
| | (Some(v), Some(h)) => |
There was a problem hiding this comment.
(Sorry for violating the instructions in the title, but in the off-chance it saves you some time, I think you can pass through an optional named argument in jsx with <SubscriptionJs variables=?v />)
There was a problem hiding this comment.
Thank you so much, so much simpler! 😄
8657691 to
b39864e
Compare
|
@kiraarghy I pulled locally and I think the issue had to do with The commit I pushed up also moves us more down the path of |
|
Okay I think this is ready for review now! |
gugahoa
left a comment
There was a problem hiding this comment.
Hello, all!
This PR would introduce some breaking change into the library, I pointed out in the comments where it happens
Another detail: The re-export of UrqlUseSubscription is missing from ReasonUrql.Hooks
It would also be really cool if there was an example of the useSubscription hook
| }; No newline at end of file | ||
| [@react.component] | ||
| let make = () => | ||
| <Subscription query variables=None handler=None> |
There was a problem hiding this comment.
This is a breaking change, variables and handler were optional args, now they're required and with type optional
Read more at UrqlSubscription.re about this
There was a problem hiding this comment.
After the changes, this should work as before:
| <Subscription query variables=None handler=None> | |
| <Subscription query> |
src/components/UrqlSubscription.re
Outdated
| external make: | ||
| ( | ||
| ~query: string, | ||
| ~variables: option(Js.Json.t), |
There was a problem hiding this comment.
When you declare a named argument with type option('a), it does not mean the argument is optional to the caller, it means it receives a option('a) type.
I believe what you wanted was: ~variables: Js.Json.t=?
| ~variables: option(Js.Json.t), | |
| ~variables: Js.Json.t=?, |
src/components/UrqlSubscription.re
Outdated
| ( | ||
| ~query: string, | ||
| ~variables: option(Js.Json.t), | ||
| ~handler: option(handler('a, 'b)), |
There was a problem hiding this comment.
The same as above:
| ~handler: option(handler('a, 'b)), | |
| ~handler: handler('a, 'b)=?, |
| ~handler: option(handler('a, 'b)), | ||
| ~children: subscriptionRenderProps('a) => React.element, | ||
| ) => | ||
| <SubscriptionJs query ?variables ?handler> |
There was a problem hiding this comment.
| <SubscriptionJs query ?variables ?handler> | |
| <SubscriptionJs query variables handler> |
There was a problem hiding this comment.
@gugahoa with your changes in place, i.e. variables as ~variables: Js.Json.t=? and ~handler: handler('a, 'b)=? I believe this can stay as is now cc/ @Schmavery.
|
Hey I'm probably not going to be able to have a look at this till tomorrow evening, given @parkerziegler's desire to get the next release I'm happy for someone else to finish it off? |
|
@kiraarghy I won't have time for release since I'm full time on client work and can only work on this stuff in the evening. I'll try to carry it through sometime tomorrow morning so we can have a v1 release ready to go for next week. We may make a second beta release as well to test these things before cutting a v1. |
1. Created UrqlUseSubscription.
1. Added rei for useSubscription. 1. Updated UqlSubscription.re to use new syntax. 1. Updated bs-platform to 5.0.4
1. Started on examples.
… and useSubscription bindings.
Didn't mean to commit :S
Co-Authored-By: Gustavo Aguiar <gugahoa@gmail.com>
Co-Authored-By: Gustavo Aguiar <gugahoa@gmail.com>
b17a778 to
8a8c98d
Compare
| }; | ||
|
|
||
| /* The signature of the structure created by calling `.make()` on a `graphql_ppx` module. */ | ||
| type request('response) = { |
There was a problem hiding this comment.
We'll want to use this type in useQuery and useMutation cc/ @gugahoa @Schmavery.
| [@bs.module "urql"] | ||
| external subscriptionComponent: ReasonReact.reactClass = "Subscription"; | ||
|
|
||
| [@bs.deriving abstract] |
There was a problem hiding this comment.
There are some follow up changes to make here, but I'm going to make a separate issue to track making the components type safe in the same manner as the hooks.
| let response = | ||
| switch (fetching, data, error) { | ||
| | (true, None, _) => UrqlTypes.Fetching | ||
| | (true, Some(d), _) => Data(d) |
There was a problem hiding this comment.
So urql has a strange case where fetching is never set to false on subscriptions (I've pinged folks internally to ensure that's intended behavior). To handle this, I needed to add this extra case to this pattern matching to check if we are receiving data even as fetching remains true.
| handler: UrqlTypes.parsedHandler('acc, 'response), | ||
| ) => { | ||
| let parse = request##parse; | ||
| let parsedHandler = |
There was a problem hiding this comment.
So the idea around parsedHandler is sort of interesting. Basically, the data that urql returns to you varies based on whether or not you provide a handler to useSubscription. If you don't provide a handler, urql will provide the raw response of executing the subscription (a Js.Json.t that we parse into 'response). If you do provide a handler urql will return whatever data structure you are using to accumulate your subscription data (referred to here as 'acc). parsedHandler, realizing this, parses each subscription as it comes in, and then passes it on to the handler supplied by the user. This ensures 1) proper type inference of subscription responses as they come in, and 2) proper type inference for the data ('acc') returned by useSubscription`. You can see an example of this in the Subscription example.
There was a problem hiding this comment.
This is a cool concept, seems like some sort of reducer for subscriptions? Out of curiosity (maybe this is out of scope), how would someone have a subscription that needs to be initialized by a query?
Like if I have an onlineUsers query and some sort of onlineUserUpdate subscription that adds or removes users from that list.
There was a problem hiding this comment.
Yep, pretty much! urql allows you to pass a handler and will let you accumulate previous subscriptions with whatever data structure you want. The crux is that, when you pass a handler, urql returns the accumulator you use to maintain data, whereas when you don't, it just returns the raw subscription result, hence the need for two separate hooks here (to get the type inference) right.
In terms of a subscription initialized by a query, the only way I can think to do it is by having the component that calls useQuery higher up in the tree than the one that calls useSubscription, and passing the result of useQuery into the child that can then pass it into useSubscription. @kitten may have other thoughts, I think there's some discussion on this over in the urql issues.
There was a problem hiding this comment.
We were thinking of the state being combined. The initial state of the subscription being the queries initial result.
We've purposefully haven't integrated the two because that can also be done with:
- a normalised cache
- a custom hook wrapping useQuery and use Subscription
- or; local component state
So we wanted to leave this choice up to the user, since it'd otherwise feel like we were over stepping the boundary of what should and can be generalised
|
@Schmavery @gugahoa @kiraarghy if any of you have some time to review the latest changes I'd really appreciate it 🙏. I took the guidance of @Schmavery's two PRs #68 and #69 to inform this one, and the type inference should be 💯 here. Let me know thoughts about the additional |
|
This PR got a little bit too big for me to effectively review, is there anywhere I can find you guys online to chat a little bit while reviewing to make sure I don't miss any context? Meanwhile I'll comment what I gathered so far |
src/UrqlTypes.re
Outdated
| `handler` corresponds to the type of the handler function _before_ the latest subscription has been parsed. | ||
| `parsedHandler` corresponds to the type of the handler function _after_ the latest subscription has been parsed. */ | ||
| type handler('acc) = | ||
| (~prevSubscriptions: 'acc, ~subscription: Js.Json.t) => 'acc; |
There was a problem hiding this comment.
Shouldn't this be option('acc)?
| (~prevSubscriptions: 'acc, ~subscription: Js.Json.t) => 'acc; | |
| (~prevSubscriptions: option('acc), ~subscription: Js.Json.t) => 'acc; |
There was a problem hiding this comment.
Yep, good catch! The way I understand ~prevSubscriptions, it will always be undefined the first time it runs, so wrapping it in option is correct 👍 Just missed this one.
|
|
||
| [@bs.module "urql"] | ||
| external useSubscriptionJs: | ||
| useSubscriptionArgs => array(useSubscriptionResponseJs) = |
There was a problem hiding this comment.
I believe it's possible do to away with useSubscriptionWithHandlerJs if we change the signature here and below with something like: (~handler: UrqlTypes.handler('acc)=?, useSubscriptionArgs) => array(useSubscriptionResponseJs)
I'm working on an example of how it could work
There was a problem hiding this comment.
Ah gotcha, so we'd have one useSubscriptionJs that takes an optional argument for ~handler for when useSubscriptionWithHandler binds to it. I could see that working!
| type useSubscriptionWithHandlerResponseJs('acc) = { | ||
| fetching: bool, | ||
| [@bs.optional] | ||
| data: 'acc, |
There was a problem hiding this comment.
Possible issue with type safety
There was a problem hiding this comment.
Can you explain a little more? I checked the type inference on the two hooks and they appeared to be working as expected. useSubscription will infer the type of data as the result of calling parse on the Js.Json.t returned by urql (written as 'response). useSubscriptionWithHandler will infer the type of the accumulator 'acc you use in your handler function. We also already parse each Js.Json.t subscription coming back from the API before handing it off to handler, so the inference should work as:
- Infer the type of
'responsefrom callingparse. - Infer the type of
'accvia thehandlerfunction (i.e. does a user supply an array, aJs.Dict, etc. for accumulating new subscriptions).
This piece is somewhat complex tho and I could definitely be missing something. Thanks as always for a thorough review ❤️
There was a problem hiding this comment.
You're completely right! I hadn't thought that useSubscriptionWithHandlerJs is safely wrapped through useSubscriptionHandler
Maybe it would be good to comment this for future contributors to take notice? Because it can still be used in a non-type safe manner, but not by the library users, only inside this module. I believe it would be good to refactor a little bit so that the unsafe surface area is minimized
|
@gugahoa agreed that the diff is getting pretty unreadable w/ all this commenting. If it works for you, I'll make any changes you'd like to see by 5pm PST today, we can merge this, and then we can tackle follow up questions in a new PR? We don't currently have any other channels setup online for discussion, tho I am considering getting a Spectrum set up for |
|
@parkerziegler it would be awesome to have a channel where we can discuss a little bit before consolidating the discussion in github comments. As for changes, how about only merging |
|
@gugahoa I'm a little reluctant to split them just because the example uses |
|
Sure! It's ok for me |
|
@parkerziegler I just had a last-minute idea re: handler. (I agree that this PR is getting hairy so feel free to ignore it for now). Assuming a signature of useSubscription that looks something like this: let useSubscription: (
~handler: UrqlTypes.parsedHandler('acc, 'response)=?,
~request: UrqlTypes.request('response)) =>
useSubscriptionResponse('acc);What if we gave subscriptions a default handler of Not 100% sure it will work but it could let us avoid having 2 entrypoints for subscriptions? |
|
@Schmavery seems like a great idea. I was trying to achieve polymorphic return with GADT, but this approach is a lot simpler and I can see it working. |
|
From what I gathered, it doesn't seem possible with a helper function: type request('b) =
| Box('b);
let barHelper = (~handler: (option('a), 'b) => 'a, ~request: request('b)) => {
let Box(r) = request;
handler(None, r)
}
let bar = (~handler: option((option('a), 'b) => 'a)=?, ~request: request('b), ()) => {
switch (handler) {
| None => barHelper(~handler=(_, x) => x, ~request)
| Some(handler_fn) => barHelper(~handler=handler_fn, ~request)
}
}The inferred type of let bar: (~handler: (option('b), 'b) => 'b=?, ~request: request('b), unit) => 'b = <fun>; |
|
Hmm, interesting, thanks for trying this. I'm gonna see if I can get something that works but feel free to ignore this for now guys |
|
@gugahoa @Schmavery I merged this since it was getting pretty darn big, and opened a new issue here for us to discuss API options. Cheers! |