1
0
mirror of synced 2025-12-19 18:11:23 -05:00

Make prefetching work with usePaginatedQuery and useInfiniteQuery (#3014)

(patch)
This commit is contained in:
Aleksandra Sikora
2021-12-03 20:54:40 +01:00
committed by GitHub
parent 4aba0d31f6
commit ad71e15290
12 changed files with 269 additions and 22 deletions

View File

@@ -15,7 +15,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - name: Use Node.js
uses: actions/setup-node@v2
with: with:
node-version: "14" node-version: "14"
- name: Count size - name: Count size

View File

@@ -56,6 +56,7 @@ const specialImports: Record<string, string> = {
useMutation: 'next/data-client', useMutation: 'next/data-client',
queryClient: 'next/data-client', queryClient: 'next/data-client',
getQueryKey: 'next/data-client', getQueryKey: 'next/data-client',
getInfiniteQueryKey: 'next/data-client',
invalidateQuery: 'next/data-client', invalidateQuery: 'next/data-client',
setQueryData: 'next/data-client', setQueryData: 'next/data-client',
useQueryErrorResetBoundary: 'next/data-client', useQueryErrorResetBoundary: 'next/data-client',

View File

@@ -11,6 +11,7 @@ export {
export { export {
queryClient, queryClient,
getQueryKey, getQueryKey,
getInfiniteQueryKey,
invalidateQuery, invalidateQuery,
setQueryData, setQueryData,
} from './react-query-utils' } from './react-query-utils'

View File

@@ -125,6 +125,23 @@ export function getQueryKey<TInput, TResult, T extends AsyncFunc>(
return getQueryKeyFromUrlAndParams(sanitizeQuery(resolver)._routePath, params) return getQueryKeyFromUrlAndParams(sanitizeQuery(resolver)._routePath, params)
} }
export function getInfiniteQueryKey<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput
) {
if (typeof resolver === 'undefined') {
throw new Error(
'getInfiniteQueryKey is missing the first argument - it must be a resolver function'
)
}
const queryKey = getQueryKeyFromUrlAndParams(
sanitizeQuery(resolver)._routePath,
params
)
return [...queryKey, 'infinite']
}
export function invalidateQuery<TInput, TResult, T extends AsyncFunc>( export function invalidateQuery<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>, resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput params?: TInput

View File

@@ -20,6 +20,7 @@ import {
QueryCacheFunctions, QueryCacheFunctions,
sanitizeQuery, sanitizeQuery,
sanitizeMutation, sanitizeMutation,
getInfiniteQueryKey,
} from './react-query-utils' } from './react-query-utils'
import { useRouter } from '../client/router' import { useRouter } from '../client/router'
@@ -166,16 +167,19 @@ export function usePaginatedQuery<
) )
} }
const suspense = const suspenseEnabled = Boolean(process.env.__BLITZ_SUSPENSE_ENABLED)
options?.enabled === false || options?.enabled === null let enabled =
isServer && suspenseEnabled
? false ? false
: options?.suspense : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const session = useSession({ suspense }) const session = useSession({ suspense })
if (session.isLoading) { if (session.isLoading) {
options.enabled = false enabled = false
} }
const routerIsReady = useRouter().isReady const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
const enhancedResolverRpcClient = sanitizeQuery(queryFn) const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getQueryKey(queryFn, params) const queryKey = getQueryKey(queryFn, params)
@@ -186,8 +190,20 @@ export function usePaginatedQuery<
: (emptyQueryFn as any), : (emptyQueryFn as any),
...options, ...options,
keepPreviousData: true, keepPreviousData: true,
enabled,
}) })
if (
queryRest.isIdle &&
isServer &&
suspenseEnabled !== false &&
!data &&
(!options || !('suspense' in options) || options.suspense) &&
(!options || !('enabled' in options) || options.enabled)
) {
throw new Promise(() => {})
}
const rest = { const rest = {
...queryRest, ...queryRest,
...getQueryCacheFunctions<FirstParam<T>, TResult, T>(queryFn, params), ...getQueryCacheFunctions<FirstParam<T>, TResult, T>(queryFn, params),
@@ -250,24 +266,26 @@ export function useInfiniteQuery<
) )
} }
const suspense = const suspenseEnabled = Boolean(process.env.__BLITZ_SUSPENSE_ENABLED)
options?.enabled === false || options?.enabled === null let enabled =
isServer && suspenseEnabled
? false ? false
: options?.suspense : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const session = useSession({ suspense }) const session = useSession({ suspense })
if (session.isLoading) { if (session.isLoading) {
options.enabled = false enabled = false
} }
const routerIsReady = useRouter().isReady const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
const enhancedResolverRpcClient = sanitizeQuery(queryFn) const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getQueryKey(queryFn, getQueryParams) const queryKey = getInfiniteQueryKey(queryFn, getQueryParams)
const { data, ...queryRest } = useInfiniteReactQuery({ const { data, ...queryRest } = useInfiniteReactQuery({
// we need an extra cache key for infinite loading so that the cache for // we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results. // for this query is stored separately since the hook result is an array of results.
// Without this cache for usePaginatedQuery and this will conflict and break. // Without this cache for usePaginatedQuery and this will conflict and break.
queryKey: routerIsReady ? [...queryKey, 'infinite'] : ['_routerNotReady_'], queryKey: routerIsReady ? queryKey : ['_routerNotReady_'],
queryFn: routerIsReady queryFn: routerIsReady
? ({ pageParam }) => ? ({ pageParam }) =>
enhancedResolverRpcClient(getQueryParams(pageParam), { enhancedResolverRpcClient(getQueryParams(pageParam), {
@@ -275,8 +293,20 @@ export function useInfiniteQuery<
}) })
: (emptyQueryFn as any), : (emptyQueryFn as any),
...options, ...options,
enabled,
}) })
if (
queryRest.isIdle &&
isServer &&
suspenseEnabled !== false &&
!data &&
(!options || !('suspense' in options) || options.suspense) &&
(!options || !('enabled' in options) || options.enabled)
) {
throw new Promise(() => {})
}
const rest = { const rest = {
...queryRest, ...queryRest,
...getQueryCacheFunctions<FirstParam<T>, TResult, T>( ...getQueryCacheFunctions<FirstParam<T>, TResult, T>(

View File

@@ -0,0 +1,33 @@
import {paginate, resolver} from "blitz"
const dataset = Array.from(Array(100).keys())
type Args = {
skip: number
take: number
where?: {value: {gte: number}}
}
let counter = 0
export default resolver.pipe(async ({skip = 0, take = 100, where}: Args) => {
counter++
const {items, hasMore, nextPage, count} = await paginate({
skip,
take,
count: async () => dataset.length,
query: async (paginateArgs) =>
dataset
.filter((i) => {
if (!where) return true
return i >= where.value.gte
})
.slice(paginateArgs.skip, paginateArgs.skip + paginateArgs.take),
})
return {
counter,
items,
hasMore,
nextPage,
count,
}
})

View File

@@ -0,0 +1,47 @@
import getPaginated from "app/queries/getPaginated"
import {
dehydrate,
getInfiniteQueryKey,
GetServerSideProps,
invokeWithMiddleware,
QueryClient,
useInfiniteQuery,
} from "blitz"
import {useState} from "react"
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const queryClient = new QueryClient()
const queryKey = getInfiniteQueryKey(getPaginated, {where: {value: {gte: 10}}, take: 5, skip: 0})
await queryClient.prefetchInfiniteQuery(queryKey, () =>
invokeWithMiddleware(getPaginated, {where: {value: {gte: 10}}, take: 5, skip: 0}, ctx),
)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Content() {
const [value] = useState(10)
const [groups] = useInfiniteQuery(
getPaginated,
(page = {take: 5, skip: 0}) => ({
where: {value: {gte: value}},
...page,
}),
{
getNextPageParam: (lastGroup) => lastGroup.nextPage,
},
)
return <div id="content">{JSON.stringify(groups)}</div>
}
function InfiniteQueryDehydratedState() {
return <Content />
}
export default InfiniteQueryDehydratedState

View File

@@ -0,0 +1,32 @@
import getMap from "app/queries/getMap"
import {
dehydrate,
getQueryKey,
GetServerSideProps,
invokeWithMiddleware,
QueryClient,
usePaginatedQuery,
} from "blitz"
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const queryClient = new QueryClient()
const queryKey = getQueryKey(getMap, undefined)
await queryClient.prefetchQuery(queryKey, () => invokeWithMiddleware(getMap, undefined, ctx))
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Content() {
const [map] = usePaginatedQuery(getMap, undefined)
return <p id="content">map is Map: {"" + (map instanceof Map)}</p>
}
function DehydratedStateWithPagination() {
return <Content />
}
export default DehydratedStateWithPagination

View File

@@ -7,7 +7,6 @@ import {
QueryClient, QueryClient,
useQuery, useQuery,
} from "blitz" } from "blitz"
import {Suspense} from "react"
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
const queryClient = new QueryClient() const queryClient = new QueryClient()
@@ -27,11 +26,7 @@ function Content() {
} }
function DehydratedState() { function DehydratedState() {
return ( return <Content />
<Suspense fallback="Loading ...">
<Content />
</Suspense>
)
} }
export default DehydratedState export default DehydratedState

View File

@@ -0,0 +1,34 @@
import getIncremented from "app/queries/getIncrementedWithPagination"
import {invalidateQuery, useInfiniteQuery} from "blitz"
import {Suspense} from "react"
function Content() {
const [groups] = useInfiniteQuery(
getIncremented,
(page = {take: 5, skip: 0}) => ({
where: {value: {gte: 10}},
...page,
}),
{
getNextPageParam: (lastGroup) => lastGroup.nextPage,
},
)
return (
<>
<button onClick={() => invalidateQuery(getIncremented)}>click me</button>
<div id="content">{JSON.stringify(groups)}</div>
</>
)
}
function InvalidateInfiniteQuery() {
return (
<div id="page">
<Suspense fallback={"Loading..."}>
<Content />
</Suspense>
</div>
)
}
export default InvalidateInfiniteQuery

View File

@@ -13,7 +13,7 @@ describe("Queries", () => {
env: {__NEXT_TEST_WITH_DEVTOOL: 1}, env: {__NEXT_TEST_WITH_DEVTOOL: 1},
}) })
const prerender = ["/use-query", "/invalidate"] const prerender = ["/use-query", "/invalidate-use-query", "/invalidate-use-infinite-query"]
await Promise.all(prerender.map((route) => renderViaHTTP(context.appPort, route))) await Promise.all(prerender.map((route) => renderViaHTTP(context.appPort, route)))
}) })
afterAll(() => killApp(context.server)) afterAll(() => killApp(context.server))
@@ -32,7 +32,7 @@ describe("Queries", () => {
describe("invalidateQuery", () => { describe("invalidateQuery", () => {
it("should invalidate the query", async () => { it("should invalidate the query", async () => {
const browser = await webdriver(context.appPort, "/invalidate") const browser = await webdriver(context.appPort, "/invalidate-use-query")
await browser.waitForElementByCss("#content") await browser.waitForElementByCss("#content")
let text = await browser.elementByCss("#content").text() let text = await browser.elementByCss("#content").text()
expect(text).toMatch(/0/) expect(text).toMatch(/0/)
@@ -75,10 +75,66 @@ describe("Queries", () => {
describe("DehydratedState", () => { describe("DehydratedState", () => {
it("should work", async () => { it("should work", async () => {
const browser = await webdriver(context.appPort, "/dehydrated-state") const browser = await webdriver(context.appPort, "/dehydrated-state-use-query")
let text = await browser.elementByCss("#content").text() let text = await browser.elementByCss("#content").text()
expect(text).toMatch(/map is Map: true/) expect(text).toMatch(/map is Map: true/)
if (browser) await browser.close() if (browser) await browser.close()
}) })
}) })
describe("DehydratedState with usePaginatedQuery", () => {
it("should work", async () => {
const browser = await webdriver(context.appPort, "/dehydrated-state-use-paginated-query")
let text = await browser.elementByCss("#content").text()
expect(text).toMatch(/map is Map: true/)
if (browser) await browser.close()
})
})
describe("DehydratedState with useInfiniteQuery", () => {
it("should work", async () => {
const browser = await webdriver(context.appPort, "/dehydrated-state-use-infinite-query")
await browser.waitForElementByCss("#content")
let text = await browser.elementByCss("#content").text()
expect(JSON.parse(text)).toEqual([
{
items: [10, 11, 12, 13, 14],
hasMore: true,
nextPage: {take: 5, skip: 5},
count: 100,
},
])
})
})
describe("invalidateQuery with useInfiniteQuery", () => {
it("should invalidate the query", async () => {
const browser = await webdriver(context.appPort, "/invalidate-use-infinite-query")
await browser.waitForElementByCss("#content")
let text = await browser.elementByCss("#content").text()
expect(JSON.parse(text)).toEqual([
{
counter: 1,
items: [10, 11, 12, 13, 14],
hasMore: true,
nextPage: {take: 5, skip: 5},
count: 100,
},
])
await browser.elementByCss("button").click()
waitFor(500)
text = await browser.elementByCss("#content").text()
expect(JSON.parse(text)).toEqual([
{
counter: 2,
items: [10, 11, 12, 13, 14],
hasMore: true,
nextPage: {take: 5, skip: 5},
count: 100,
},
])
if (browser) await browser.close()
})
})
}) })