gql-dart / ferry

Stream-based strongly typed GraphQL client for Dart
https://ferrygraphql.com/
MIT License
604 stars 116 forks source link

Hooks support #158

Closed kallaspriit closed 3 years ago

kallaspriit commented 3 years ago

Great work on Ferry! A quick feature request here :)

I come from React & Apollo background and like many others I'm using GraphQL Code Generator that fulfills similar purpose to Ferry. One really nice feature of it is generating hooks based on queries and mutation.

For example when I write a mutation like:

mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    ...UserInfo
  }
}

It generates a hook useLoginMutation that can be used as

// the function name is generated from mutation name
const [login, loginResult] = useLoginMutation({
  refetchQueries: ["Viewer"], // refetching queries by name would be great also!
  awaitRefetchQueries: true, // and awaiting these refetched queries
});

// execute the mutation (on button click / form submit etc)
const response = await login({
  variables: { email, password },
});

// loginResult has data, loading, error

Similar with queries:

query Viewer {
  viewer {
    ...UserInfo
  }
}
const { data, loading, error } = useViewerQuery();

Since flutter also supports hooks through flutter_hooks it would be really nice if generating such hooks could be enabled with some configuration option.

Using hooks (for graphql) results in much less code and less component nesting, once I tried it I don't want to do it any other way :)

I'm working on my own generic useQuery and useMutation hooks that can be used as

final viewerQuery = useQuery(context, GViewerReq());

// handle error
if (viewerQuery.hasError) {
  return ErrorScreen(
    title: 'Loading viewer info failed',
    message: viewerQuery.errorMessage,
  );
}

// get logged in user
final viewer = viewerQuery.response?.data?.viewer;
final isLoggedIn = viewer != null;

And mutations

final loginMutation = useMutation(
  context,
  GLoginReq(
    (b) => b
      ..vars.email = email.value
      ..vars.password = password.value,
  ),
  refetchQueries: [viewerQuery], // refetching by name would be nicer, this requires the other query to be in scope
);

// execute the mutation (on button click / form submit etc)
loginMutation.run();

// get logged in user
final loginViewer = loginMutation.response?.data?.login;

Which works but would be great if the hooks would be generated by Ferry :)

smkhalsa commented 3 years ago

Thanks for the input!

This is definitely something that could be layered on top of ferry as a separate builder package.

If you're interested in adding this to your GraphQL hooks package, I'd be glad to guide you on the builder integration.

kallaspriit commented 3 years ago

Hey, if this sounds interesting enough to be added by Ferry developers then that would be great.

If not then maybe I can help but I'm still quite new to Flutter and Dart, will take some time to feel more comfortable :)

kallaspriit commented 3 years ago

Hey!

I'm continuing work useQuery and useMutation hooks but I'm a bit stuck trying to provide variables to mutation when calling it.

Basically I've got this pattern working fine:

final loginMutation = useMutation(
  context,
  // request variables are set when creating GLoginReq
  GLoginReq(
    (mutation) => mutation
      ..vars.email = email.value
      ..vars.password = password.value,
  ),
  refetchQueries: ['Viewer'],
);

...

// run mutation on form submit etc
loginMutation.run();

But what I'm trying to also support is providing the variables to the mutation.run method so it would rebuild the query with updated variables. Something like:

final loginMutation = useMutation(
  context,
  // request variables are set to initial values but overridden in run()
  GLoginReq(
    (mutation) => mutation
      ..vars.email = ''
      ..vars.password = '',
  ),
  refetchQueries: ['Viewer'],
);

...

// run mutation on form submit etc, overriding variables
loginMutation.run(
  vars: GLoginVars((b) => b
    ..email = email.value
    ..password = password.value,
  ),
)

Right now the query argument to useQuery is OperationRequest<TData, TVars> but this does not have the rebuild method. How can I type it properly so I could use it in client.request(query) but also call query.rebuild((b) => b.vars = vars)?

smkhalsa commented 3 years ago

I'd probably just cast to dynamic before calling rebuild. Not pretty, but it should work.

kallaspriit commented 3 years ago

Yeah that's what I did. It does work but yeah not pretty. Oh well :)

smkhalsa commented 3 years ago

Closing this issue, but @kallaspriit let me know if you do publish your hooks package, and we can link to it in the readme.

johannbuscail commented 3 years ago

@smkhalsa Any update on hook support ?

hansihe commented 1 month ago

For anyone interested in this, here is a dead simple hook that should mostly match the behavior of the Operation widget:

OperationResponse<TData, TVars> useGraphql<TData, TVars>(Client client, OperationRequest<TData, TVars> request) {
  var stream = useMemoized(() {
    return client.request(request).distinct();
  }, [request]);

  var snapshot = useStream(stream);

  if (snapshot.hasData) {
    return snapshot.data!;
  } else if (snapshot.hasError) {
    // Should never happen, GqlTypedLink should intercept stream errors
    throw "unreachable";
  } else {
    // Return a response in the loading state
    return OperationResponse(
        operationRequest: request
    );
  }
}

Usage is as follows (from your HookWidget):

var client = getGraphqlClient();
var startupData = useGraphql(client, GMyQuery());