Skip to content

Commit

Permalink
Fix queryKeyGeneration when using invalidateQuery (#3728)
Browse files Browse the repository at this point in the history
Co-authored-by: beerose <[email protected]>
  • Loading branch information
Zeko369 and beerose authored Oct 11, 2022
1 parent d98e4ba commit aa34661
Show file tree
Hide file tree
Showing 9 changed files with 566 additions and 59 deletions.
6 changes: 6 additions & 0 deletions .changeset/tame-pumpkins-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@blitzjs/rpc": patch
"blitz": patch
---

Fix invalidateQuery generating wrong param when no param argument is passed
1 change: 1 addition & 0 deletions .changeset/thirty-spies-applaud.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
"@blitzjs/rpc": patch
"blitz": patch
---

Migrate over recipe functionality from legacy framework & expose recipe builder helper functions that find and modify next.config.js, blitz-server & blitz-client.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const cache = {}

export default async function getSequence(key: string) {
cache[key] = cache[key] || 0
return cache[key]++
}
55 changes: 55 additions & 0 deletions integration-tests/react-query-utils/pages/page-with-invalidate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, {Suspense} from "react"
import {BlitzPage} from "@blitzjs/next"
import {invalidateQuery, useQuery} from "@blitzjs/rpc"
import getSequence from "../app/queries/getSequence"

const useQueryOptions = {
refetchInterval: 0,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}

const PageWithInvalidateQuery: React.FC = () => {
const [query1, {isFetching: isQ1Fetching}] = useQuery(getSequence, "query1", useQueryOptions)
const [query2, {isFetching: isQ2Fetching}] = useQuery(getSequence, "query2", useQueryOptions)

const isFetching = isQ1Fetching || isQ2Fetching

const onRevalidateBoth = async () => {
await invalidateQuery(getSequence)
}
const onRevalidateFirst = async () => {
await invalidateQuery(getSequence, "query1")
}

return (
<div>
<h1>Hello from PageWithInvalidateQuery</h1>
<button id="invalidate-both" onClick={onRevalidateBoth}>
Both
</button>
<button id="invalidate-first" onClick={onRevalidateFirst}>
First
</button>

{isFetching && <h3>Loading...</h3>}
{!isFetching && (
<div id="data">
<h2 id="data-first">{query1}</h2>
<h2 id="data-second">{query2}</h2>
</div>
)}
</div>
)
}

const PageWithInvalidateQueryPage: BlitzPage = () => {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<PageWithInvalidateQuery />
</Suspense>
)
}

export default PageWithInvalidateQueryPage
84 changes: 60 additions & 24 deletions integration-tests/react-query-utils/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@ import {describe, it, expect, beforeAll, afterAll} from "vitest"
import {
killApp,
findPort,
launchApp,
nextBuild,
nextStart,
runBlitzCommand,
blitzLaunchApp,
blitzBuild,
blitzStart,
} from "../../utils/next-test-utils"
import webdriver from "../../utils/next-webdriver"

import {join} from "path"

let app: any
let appPort: number
const appDir = join(__dirname, "../")

const runTests = (mode?: string) => {
const runTests = () => {
describe("get query data", () => {
it(
"should work",
Expand All @@ -36,23 +30,65 @@ const runTests = (mode?: string) => {
},
5000 * 60 * 2,
)
}),
describe("prefetch infinite query", () => {
it(
"should work",
async () => {
const browser = await webdriver(appPort, "/page-with-prefetch-inf-query")

browser.waitForElementByCss("#data", 0)
const newText = await browser.elementByCss("#data").text()
expect(newText).not.toMatch("no-data")
expect(newText).toMatch("thanks")

if (browser) await browser.close()
},
5000 * 60 * 2,
)
})
})

describe("prefetch infinite query", () => {
it(
"should work",
async () => {
const browser = await webdriver(appPort, "/page-with-prefetch-inf-query")

browser.waitForElementByCss("#data", 0)
const newText = await browser.elementByCss("#data").text()
expect(newText).not.toMatch("no-data")
expect(newText).toMatch("thanks")

if (browser) await browser.close()
},
5000 * 60 * 2,
)
})

describe("invalidate query", () => {
it(
"should work",
async () => {
const browser = await webdriver(appPort, "/page-with-invalidate")
const getData = async () => {
const q1 = await browser.elementByCss("#data-first").text()
const q2 = await browser.elementByCss("#data-second").text()

return {q1: parseInt(q1), q2: parseInt(q2)}
}

browser.waitForElementByCss("#data", 0)

const initialData = await getData()
expect(initialData.q1).equal(0)
expect(initialData.q2).equal(0)

browser.elementByCss("#invalidate-both").click() // sometimes first one returns the same value
await new Promise((r) => setTimeout(r, 100))
browser.elementByCss("#invalidate-both").click()

browser.waitForElementByCss("#data", 0)

const bothData = await getData()
expect(bothData.q1).greaterThan(initialData.q1)
expect(bothData.q2).greaterThan(initialData.q2)

browser.elementByCss("#invalidate-first").click()
browser.waitForElementByCss("#data", 0)

const afterSecond = await getData()
expect(afterSecond.q1).equal(bothData.q1 + 1)
expect(afterSecond.q2).equal(bothData.q2)

if (browser) await browser.close()
},
5000 * 60 * 2,
)
})
}

describe("React Query Utils Tests", () => {
Expand Down
77 changes: 77 additions & 0 deletions packages/blitz-rpc/src/data-client/react-query-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {describe, expect, it} from "vitest"
import superJson from "superjson"

import {getQueryKey, getQueryKeyFromUrlAndParams} from "./react-query-utils"
import {RpcClient} from "./rpc"

const API_ENDPOINT = "http://localhost:3000"

const constructData = (arg: any) => {
return {
data: arg,
expected: superJson.serialize(arg),
}
}

describe("react-query-utils", () => {
describe("getQueryKeyFromUrlAndParams", () => {
it("returns a query key with string arg", () => {
const {data, expected} = constructData("RandomString")
expect(getQueryKeyFromUrlAndParams(API_ENDPOINT, data)).toEqual([API_ENDPOINT, expected])
})

it("returns a query key with object arg", () => {
const {data, expected} = constructData({id: 1, name: "test", field: undefined})
expect(getQueryKeyFromUrlAndParams(API_ENDPOINT, data)).toEqual([API_ENDPOINT, expected])
})

it("returns a query key with undefined arg", () => {
const {data, expected} = constructData(undefined)
expect(getQueryKeyFromUrlAndParams(API_ENDPOINT, data)).toEqual([API_ENDPOINT, expected])
})

it("returns a query key with null arg", () => {
const {data, expected} = constructData(null)
expect(getQueryKeyFromUrlAndParams(API_ENDPOINT, data)).toEqual([API_ENDPOINT, expected])
})

it("if no argument is passed it returns only url", () => {
const queryKey = getQueryKeyFromUrlAndParams(API_ENDPOINT)
expect(queryKey).toEqual([API_ENDPOINT])
})
})

describe("getQueryKey", () => {
// @ts-expect-error - we just need these 3 params
const query: RpcClient<{}, null> = {
_resolverName: "randomQuery",
_resolverType: "query",
_routePath: API_ENDPOINT,
}

it("returns a query key with string arg", () => {
const {data, expected} = constructData("RandomString")
expect(getQueryKey(query, data)).toEqual([API_ENDPOINT, expected])
})

it("returns a query key with object arg", () => {
const {data, expected} = constructData({id: 1, name: "test", field: undefined})
expect(getQueryKey(query, data)).toEqual([API_ENDPOINT, expected])
})

it("returns a query key with undefined arg", () => {
const {data, expected} = constructData(undefined)
expect(getQueryKey(query, data)).toEqual([API_ENDPOINT, expected])
})

it("returns a query key with null arg", () => {
const {data, expected} = constructData(null)
expect(getQueryKey(query, data)).toEqual([API_ENDPOINT, expected])
})

it("if no argument is passed it returns only url", () => {
const queryKey = getQueryKey(query)
expect(queryKey).toEqual([API_ENDPOINT])
})
})
})
37 changes: 24 additions & 13 deletions packages/blitz-rpc/src/data-client/react-query-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {QueryClient, QueryFilters} from "@tanstack/react-query"
import {QueryClient} from "@tanstack/react-query"
import {serialize} from "superjson"
import {isClient, isServer, AsyncFunc} from "blitz"
import {ResolverType, RpcClient} from "./rpc"
Expand Down Expand Up @@ -124,24 +124,29 @@ const sanitize =
export const sanitizeQuery = sanitize("query")
export const sanitizeMutation = sanitize("mutation")

export const getQueryKeyFromUrlAndParams = (url: string, params: unknown) => {
const queryKey = [url]

const args = typeof params === "function" ? (params as Function)() : params
queryKey.push(serialize(args) as any)
type BlitzQueryKey = [string] | [string, any]
export const getQueryKeyFromUrlAndParams = (
url: string,
...params: [unknown] | []
): BlitzQueryKey => {
const queryKey: BlitzQueryKey = [url]
if (params.length === 1) {
const param = params[0]
queryKey.push(serialize(typeof param === "function" ? param() : param) as any)
}

return queryKey as [string, any]
return queryKey
}

export function getQueryKey<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput,
...params: [TInput] | []
) {
if (typeof resolver === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a resolver function")
}

return getQueryKeyFromUrlAndParams(sanitizeQuery(resolver)._routePath, params)
return getQueryKeyFromUrlAndParams(sanitizeQuery(resolver)._routePath, ...params)
}

export function getInfiniteQueryKey<TInput, TResult, T extends AsyncFunc>(
Expand All @@ -158,17 +163,23 @@ export function getInfiniteQueryKey<TInput, TResult, T extends AsyncFunc>(
return [...queryKey, "infinite"]
}

export function invalidateQuery<TInput, TResult, T extends AsyncFunc>(
type InvalidateQueryTypeWithParams = <TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput,
) {
...params: [TInput]
) => Promise<void>
type InvalidateQueryTypeAllQueries = <TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
) => Promise<void>
type InvalidateQueryType = InvalidateQueryTypeWithParams & InvalidateQueryTypeAllQueries

export const invalidateQuery: InvalidateQueryType = (resolver, ...params: []) => {
if (typeof resolver === "undefined") {
throw new Error(
"invalidateQuery is missing the first argument - it must be a resolver function",
)
}

const fullQueryKey = getQueryKey(resolver, params)
const fullQueryKey = getQueryKey(resolver, ...params)
return getQueryClient().invalidateQueries(fullQueryKey)
}

Expand Down
Loading

0 comments on commit aa34661

Please sign in to comment.