Skip to content
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

How to handle dependent queries? #1941

Open
lukasbash opened this issue Oct 6, 2024 · 7 comments
Open

How to handle dependent queries? #1941

lukasbash opened this issue Oct 6, 2024 · 7 comments
Labels
enhancement New feature or request openapi-react-query Relevant to openapi-react-query

Comments

@lukasbash
Copy link

We are already using the openapi-fetch package to generate our client code for the specification.
We want to establish the usage of react query too, so we're glad that you work on a component here.
Basically we have a very granular backend API which results in a lot of dependent query calls.
How is this handled in your query package? Usually the query options are mixed with additional flags, in this case I am aware of enabled and skipToken. But as the specification might contain mandatory parameters, it can't just work out.

Any feedback on this?

@kerwanp
Copy link
Contributor

kerwanp commented Oct 15, 2024

Hi, I think the best thing I can do is redirect you to the documentation https://openapi-ts.pages.dev/openapi-react-query/

But for a quick example, you can either use:

const { data } = $query.useQuery('get', '/users', {}, { placeholder: keepPreviousData, enabled: true })
const { data } = useQuery($query.queryOptions('get', '/users', {}, { placeholder: keepPreviousData, enabled: true }))

Both allow you to use query options properly.

@kerwanp kerwanp added the openapi-react-query Relevant to openapi-react-query label Oct 15, 2024
@lukasbash
Copy link
Author

lukasbash commented Oct 15, 2024

Hi, I think the best thing I can do is redirect you to the documentation https://openapi-ts.pages.dev/openapi-react-query/

But for a quick example, you can either use:

const { data } = $query.useQuery('get', '/users', {}, { placeholder: keepPreviousData, enabled: true })

const { data } = useQuery($query.queryOptions('get', '/users', {}, { placeholder: keepPreviousData, enabled: true }))

Both allow you to use query options properly.

Well, maybe I did not point out the problem precisely, but it is not related to being able to pass the proper parameters to the query engine. The generated code from e.g. an endpoint specification might contain non optional values. Even if we can pass the enabled flag as a parameter, we cannot get the compiler working with the actual endpoint parameters.

Given an endpoint with the signature /api/users/{id}, of course we can disable the actual query execution but the path parameter is not optional.

Of course one fix could be to just specify everything optional in the schema but this is not only wrong API design but also requires much much more coding overhead.

Is my example understandable?

@kerwanp
Copy link
Contributor

kerwanp commented Oct 15, 2024

Required parameters must always be defined. For example:

const { data } = $query.useQuery('get', '/users/:id', {}) // Typescript shouting
const { data } = $query.useQuery('get', '/users/:id', { params: { path: { id: 4 } } }}) // All good

And it will work the same with body parameters, query params, etc. Thanks to openapi-fetch and openapi-typescript.

@lukasbash
Copy link
Author

lukasbash commented Oct 16, 2024

Required parameters must always be defined. For example:

const { data } = $query.useQuery('get', '/users/:id', {}) // Typescript shouting
const { data } = $query.useQuery('get', '/users/:id', { params: { path: { id: 4 } } }}) // All good

And it will work the same with body parameters, query params, etc. Thanks to openapi-fetch and openapi-typescript.

Yeah exactly thats the problem in combination with the enabled or skipToken flag.

Given the need of a dependent query you can only trick the compiler into a valid statement. Let me show you a quick example:

// Initial query, might be anything else which can be pending
const { data: users } = $query.useQuery('get', '/users', {});

// Find a property where any follow-up query depends on
const id = (users || [] ).find(u => u.cool)?.id;

// Disable the query unless the property is there
// ISSUE: Must trick compiler with "id!" to keep it clean!!
const { data } = $query.useQuery('get', '/users/:id', { params: { path: { id: id! } } }, { enabled: !!id })

For further understanding of the need for a feature/enhancement for this have a read at e.g. https://tkdodo.eu/blog/react-query-and-type-script#type-safety-with-the-enabled-option
Given the example above we would usually create own hooks with own optional parameter configs and handle the type safety on our own, but as the package now generates the query calls for us, it is just a hassle.

More understandable now? I just want to suggest a way to bake in type safety into the package, so that parameters which are relevant for a query which is not enabled, might be optional.

EDIT:
I just thought about a quick wrapper you could use internally, specifically if an enabled flag is set. What do you think about it?

function dependOn<T, R>(
  dependency: T | undefined,
  queryFn: (dependency: T) => Promise<R>,
): () => Promise<R> {
  return typeof dependency === "undefined"
    ? () => Promise.reject(new Error("Dependency not passed to query function"))
    : () => queryFn(dependency);
}

@kerwanp
Copy link
Contributor

kerwanp commented Oct 16, 2024

Okay now I actually understand your point. In fact I actually encountered that multiple time. And I actually used ! as in your example thinking that we should need to find a proper solution for that.

The goal of openapi-typescript and openapi-fetch is to give type-safety, not runtime safety. This is why it is so lightweight, because it primarily bases everything on types. Meaning we can't know at runtime if something is required or not.

Your proposed solution is interesting but I don't see how we could use that internally as we don't have the information about required or mandatory types. We could expose a similar function for that:

$query.useQuery(
  "get",
  "/users/:id",
  ...dependsOn([id, filter], ([id, filter]) => ({
    params: { path: { id }, query: { filter } },
  }))
);

The first parameter is a list of T|undefined|null variables that is used for the enabled option.
The second parameter is to resolve the parameters and the list of dependencies are passed as parameter but they are defined.

And this should return:

$query.useQuery(
  "get",
  "/users/:id",
  { params: { path: { id }, query: { filter } } },
  { enabled: id && filter }
);

What do you think?

@kerwanp kerwanp added the enhancement New feature or request label Oct 16, 2024
@zsugabubus
Copy link
Contributor

zsugabubus commented Oct 19, 2024

Some ideas:

  1. id ? <User userId={id} /> : <NoUser />.
  2. {params: {path: {id: id ?? 0}}}.
const SKIP = {queryKey: [] as any, queryFn: skipToken} as const;
useQuery(id ? $query.queryOptions(...) : SKIP);

@lukasbash
Copy link
Author

Okay now I actually understand your point. In fact I actually encountered that multiple time. And I actually used ! as in your example thinking that we should need to find a proper solution for that.

The goal of openapi-typescript and openapi-fetch is to give type-safety, not runtime safety. This is why it is so lightweight, because it primarily bases everything on types. Meaning we can't know at runtime if something is required or not.

Your proposed solution is interesting but I don't see how we could use that internally as we don't have the information about required or mandatory types. We could expose a similar function for that:

$query.useQuery(
  "get",
  "/users/:id",
  ...dependsOn([id, filter], ([id, filter]) => ({
    params: { path: { id }, query: { filter } },
  }))
);

The first parameter is a list of T|undefined|null variables that is used for the enabled option. The second parameter is to resolve the parameters and the list of dependencies are passed as parameter but they are defined.

And this should return:

$query.useQuery(
  "get",
  "/users/:id",
  { params: { path: { id }, query: { filter } } },
  { enabled: id && filter }
);

What do you think?

I like the idea. As long as we can keep the correct type inference of the returned object it is fine. Also I think this should just be an overloaded signature as there are still lot of query cases which are not dependent. In fact the dependsOn must be a baked in utility in my opinion, so it should be exported from the package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request openapi-react-query Relevant to openapi-react-query
Projects
None yet
Development

No branches or pull requests

3 participants