Make prefetching work with usePaginatedQuery and useInfiniteQuery (#3014)
(patch)
This commit is contained in:
committed by
GitHub
parent
4aba0d31f6
commit
ad71e15290
3
.github/workflows/compressed.yml
vendored
3
.github/workflows/compressed.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
queryClient,
|
queryClient,
|
||||||
getQueryKey,
|
getQueryKey,
|
||||||
|
getInfiniteQueryKey,
|
||||||
invalidateQuery,
|
invalidateQuery,
|
||||||
setQueryData,
|
setQueryData,
|
||||||
} from './react-query-utils'
|
} from './react-query-utils'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user