Closed chrishale closed 4 weeks ago
On reflection I'm not sure if this is really the best solve, as it might break in earlier or future versions of react-query.
Because the use of react-query is optional, and a strict version isn't a peer dependancy of graphql-zeus
it's open to using the wrong version.
My personal preference would be for these "plugins" to be separate dependancies, so each one could have the exact version of it's client as a dependancy.
But once you've got a grasp of graphql-zeus's types, it's not that difficult to write your own, and given the QueryKey type shouldn't change much (you'd just have one for Query/QueryRoot or whatever your graphql server defines it as, and one for Mutation/MutationRoot).
So, I have been writing my own useTypedQuery
and useTypedMutation
in my own source code, alongside the generated code.
I'd typically have a file where I use Thunder to create a custom client, then export a query and mutation function that I would use server side, and the hooks for client side. My latest implementation is something like:
const client = Thunder(async query => {})
const Scalars = ZeusScalars({
...
})
export const query = () => client('query', { scalars: Scalars })
export const mutation = () => client('mutation', { scalars: Scalars })
export function useTypedQuery<
O extends 'Query',
TData extends ValueTypes[O],
TResult = InputType<GraphQLTypes[O], TData>,
>(
queryKey: QueryKey,
queryGraph: TData | ValueTypes[O],
options?: Omit<UseQueryOptions<TResult>, 'queryKey' | 'queryFn'>,
zeusOptions?: OperationOptions,
) {
return useQuery<TResult>({
queryKey,
queryFn: () =>
client('query', { scalars: Scalars })(
queryGraph,
zeusOptions,
) as Promise<TResult>,
...options,
})
}
export function useTypedMutation<
O extends 'Mutation',
TData extends ValueTypes[O],
TResult = InputType<GraphQLTypes[O], TData>,
TVariables = ExtractVariables<TData>,
>(
mutationKey: MutationKey,
mutation: TData | ValueTypes[O],
options?: Omit<
UseMutationOptions<TResult, unknown, TVariables>,
'mutationKey' | 'mutationFn'
>,
zeusOptions?: OperationOptions,
) {
return useMutation<TResult, unknown, TVariables>({
mutationKey,
mutationFn: variables =>
client('mutation', { scalars: Scalars })(mutation, {
...zeusOptions,
variables: variables as Record<string, undefined>,
}) as Promise<TResult>,
...options,
})
}
I have to agree that generating these functions are problematic since there are multiple different versions of react query. I have also made my own versions of these functions in my codebase since it makes it easier to supply scalars and the api url. However its quite difficult to do it correctly since there is a lot of types that need to be in the right place.
For example in your implementation you did not add the type of Scalars
to your TResult
, so I dont think you will get the correct types for scalar fields. I had a different mistake in my implementation that caused me a lot of issues for some time. https://github.com/graphql-editor/graphql-zeus/discussions/374
An alternative solution might be to just add examples, either in the repo or in the documentation that should be copy-pasted into your code base. The react-query specific code is not dynamic anyway, and it makes it more clear that its your responsibility to keep it up to date if you change versions of react-query or other dependencies.
For example in your implementation you did not add the type of Scalars to your TResult, so I dont think you will get the correct types for scalar fields. I had a different mistake in my implementation that caused me a lot of issues for some time.
@reite Thanks for spotting this, I have to admit I've not actually had a great deal of use for Scalars in the projects I've used this with. But in the one instance the GraphQL server had a DateTime scalar, I was wondering why it was cast as unknown
.
I guess you'd just need to update the TResult type to something like this?
TResult = InputType<GraphQLTypes[O], TData, typeof Scalars>
I guess you'd just need to update the TResult type to something like this?
TResult = InputType<GraphQLTypes[O], TData, typeof Scalars>
Yes that's right.
Another issue with your implementation is that it will give a type error if you to use a react-query select
option to refine your result. To support that you need another type variable which allow the end result to be different from the query function result. My implementation currently looks similar to this:
// My real query function is a bit more complicated than this.
const query = (op) => Chain(url, options)("query", {scalars})(op)
function useTypedQuery<
O extends "query_root",
TData extends ValueTypes[O],
TQResult = InputType<GraphQLTypes[O], TData, typeof scalars>,
TResult = TQResult
>(
queryKey: unknown[],
q: TData | ValueTypes[O],
options?: Omit<
UseQueryOptions<TQResult, unknown, TResult>,
"queryKey" | "queryFn"
>
) {
return useQuery<TQResult, unknown, TResult>(
queryKey,
() => query(q),
options
);
}
My main point here is that its pretty difficult to get this right, and I am far from sure I have gotten it right as I dont have deep enough knowledge of zeus and react-query to be sure. I am just improving my implementation as I go along.
Personally I find the current react-query generated code to be very lacking for anything except as a starting out point. I am thinking about how we can improve it. The fact that it is a generated makes it more difficult to make improvements. I think a better solution could be to write some documentation or a blog post on how to best use zeus with react query, and expect people to just copy the code instead of generating it. That way we could be explicit about dependencies. What do you @aexol think? Would you prefer to keep the current generated approach or are you open to a different solution?
Currently the react-query implementation only works with react-query v2. In v3 the generic types passed to the hooks have changed.
They're optional so the two extra
any
types aren't needed, but they override the TResult that is used to describe the data.