-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UrqlUseSubscription #60
Conversation
kiraarghy
commented
Jun 5, 2019
- Created UrqlUseSubscription.
src/components/UrqlSubscription.re
Outdated
) => | ||
switch (variables, handler) { | ||
| (Some(v), Some(h)) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
[@react.component] | ||
let make = () => | ||
<Subscription query variables=None handler=None> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same as above:
~handler: option(handler('a, 'b)), | |
~handler: handler('a, 'b)=?, |
~children: subscriptionRenderProps('a) => React.element, | ||
) => | ||
<SubscriptionJs query ?variables ?handler> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<SubscriptionJs query ?variables ?handler> | |
<SubscriptionJs query variables handler> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
`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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible issue with type safety
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
'response
from callingparse
. - Infer the type of
'acc
via thehandler
function (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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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! |