SvelteStack / svelte-query

Performant and powerful remote data synchronization for Svelte
https://sveltequery.vercel.app
MIT License
813 stars 31 forks source link

Feature request: Use generator functions for dependent queries #100

Open NatoBoram opened 1 year ago

NatoBoram commented 1 year ago

I'm working on a page with 2 dependent queries. One depends on a page parameter and one depends on another query. If I roughly follow the tutorial, I get this:

// Root profile type
const rootProfileTypeQuery = useQuery(
  `profile-type:${PUBLIC_ROOT_PROFILE_TYPE_ID}`,
  () => getProfileType(PUBLIC_ROOT_PROFILE_TYPE_ID)
)
$: rootProfileType = $rootProfileTypeQuery.data

// Profile
$: id = $page.params["id"]
const profileQuery = useQuery(`profile:${id}`, () => getProfile(id as string), {
  enabled: !!id,
})
$: profileQuery.setOptions(`profile:${id}`, () => getProfile(id as string), {
  enabled: !!id,
})
$: profile = $profileQuery?.data

// Profile type
const profileTypeQuery = useQuery(
  `profile-type:${profile?.profile_type.id}`,
  () => getProfileType(profile?.profile_type.id as string),
  { enabled: !!profile }
)
$: profileTypeQuery.setOptions(
  `profile-type:${profile?.profile_type.id}`,
  () => getProfileType(profile?.profile_type.id as string),
  { enabled: !!profile }
)
$: profileType = $profileTypeQuery?.data

There's an apparent problem: I have to re-type the entire thing every time something changes.

One could just re-create the entire query reactively, but it doesn't feel right. There must be a reason for the enabled parameter to exist.

// Root profile type
const rootProfileTypeQuery = useQuery(
  `profile-type:${PUBLIC_ROOT_PROFILE_TYPE_ID}`,
  () => getProfileType(PUBLIC_ROOT_PROFILE_TYPE_ID)
)
$: rootProfileType = $rootProfileTypeQuery.data

// Profile
$: id = $page.params["id"]
$: profileQuery = useQuery(`profile:${id}`, () => getProfile(id as string), {
  enabled: !!id,
})
$: profile = $profileQuery?.data

// Profile type
$: profileTypeQuery = useQuery(
  `profile-type:${profile?.profile_type.id}`,
  () => getProfileType(profile?.profile_type.id as string),
  { enabled: !!profile }
)
$: profileType = $profileTypeQuery?.data

Working with this doesn't feel good and I'm trying to find solutions to tap in SvelteQuery's great features but with an API that's a bit less insane.

What if it was generated on the fly?

When a query is created, we know what we want to send it even if we don't have it yet. Example:

// Profile type
const profileTypeQuery = useQuery(
    `profile-type:${profile?.profile_type.id}`,
    () => getProfileType(profile?.profile_type.id as string),
    { enabled: !!profile },
)
$: profileTypeQuery.setOptions(
    `profile-type:${profile?.profile_type.id}`,
    () => getProfileType(profile?.profile_type.id as string),
    { enabled: !!profile },
)
$: profileType = $profileTypeQuery?.data

Why not use generator functions to generate the ID and the fetch only when enabled is turned on?

// Profile type
const profileTypeQuery = generateUseQuery(
    profile,
    profile => `profile-type:${profile?.profile_type.id}`,
    profile => getProfileType(profile?.profile_type.id),
    profile => ({ enabled: !!profile }),
)
$: profileTypeQuery.setInput(profile)
$: profileType = $profileTypeQuery?.data

This isn't necessarily shorter than the other ones, but it's definitely cleaner and it can be put inside a function somewhere and imported in a component.

// api.ts
export function queryProfileType(profile?: Profile) {
    return generateUseQuery(
        profile,
        profile => `profile-type:${profile?.profile_type.id}`,
        profile => getProfileType(profile?.profile_type.id),
        profile => ({ enabled: !!profile }),
    )
}

// +page.svelte
const profileTypeQuery = queryProfileType(profile)
$: profileTypeQuery.setInput(profile)
$: profileType = $profileTypeQuery?.data

And now we can use Svelte Query in 2 lines from a component's perspective. Doing this with the current API requires quite a lot of boilerplate that could be saved by generator functions.

// api.ts
function queryProfileType(id?: string) {
    return useQuery(`profile-type:${id}`, () => getProfileType(id as string), { enabled: !!id })
}

function setOptionsProfileType(
    query: UseQueryStoreResult<ProfileType, unknown, ProfileType, `profile-type:${string}`>,
    id?: string,
) {
    return query.setOptions(`profile-type:${id}`, () => getProfileType(id ?? ''), {
        enabled: !!profile,
    })
}

// Profile type
const profileTypeQuery = queryProfileType(profile?.id)
$: setOptionsProfileType(profileTypeQuery, profile?.profile_type.id)
$: profileType = $profileTypeQuery?.data

One could make generator functions to avoid duplicating code in api.ts but it's even more boilerplate for something that should be simple.

function queryId(id?: string): `profile-type:${string}` {
    return `profile-type:${id}`
}

function queryFetch(id?: string) {
    return () => getProfileType(id ?? '')
}

function queryOptions(id?: string) {
    return { enabled: !!id }
}

export function queryProfileType(id?: string) {
    return useQuery(queryId(id), queryFetch(id), queryOptions(id))
}

export function setQueryProfileType(
    query: UseQueryStoreResult<ProfileType, unknown, ProfileType, `profile-type:${string}`>,
    id?: string,
) {
    return query.setOptions(queryId(id), queryFetch(id), queryOptions(id))
}

// All of this to be able to do
const profileTypeQuery = queryProfileType(profile?.profile_type.id)
$: setQueryProfileType(profileTypeQuery, profile?.profile_type.id)