diff --git a/readme.md b/readme.md index a6ea959..872c739 100644 --- a/readme.md +++ b/readme.md @@ -89,6 +89,31 @@ export function UserList() { } ``` +### Example usage with Next.js, zod and no default query + +Notice that we let zod take a string and then coerce it to a number. If the coercion fails, we do not stop the whole parsing, but default the property to `undefined`. +This allows us to still use `departmentId` if it is defined while `userId` is not and vice versa. + +```tsx +import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/zod/routers/nextRouter' +import { z } from 'zod' + +export function Component() { + const { page, size, query, setQuery, setPage, resetQuery } = + useQueryAndPagination( + z.object({ + departmentId: z + .string() + .pipe(z.coerce.number().optional()) + .catch(undefined), + userId: z.string().pipe(z.coerce.number().optional()).catch(undefined), + }), + ) + + // ... do something +} +``` + ### Example usage with React Router and zod ```tsx diff --git a/src/engine/pagination.ts b/src/engine/pagination.ts index 4ba0a3e..2081242 100644 --- a/src/engine/pagination.ts +++ b/src/engine/pagination.ts @@ -52,7 +52,7 @@ export const useAbstractQueryAndPagination = < >( router: Router, parseQuery: ParseQuery, - defaultQuery: TDefaultQuery, + defaultQuery: TDefaultQuery = {} as TDefaultQuery, defaultPagination?: Partial, options?: Partial, ) => { diff --git a/src/engine/query.ts b/src/engine/query.ts index 9e85bdf..06b7c88 100644 --- a/src/engine/query.ts +++ b/src/engine/query.ts @@ -35,10 +35,10 @@ export type Router = { } export type RouterSSR = Router & { - isReady: boolean + isQueryReady: boolean } -export const useQuery = (router: Router, defaultQuery: T) => { +export const useQuery = (router: Router, defaultQuery: Query) => { const resetQuery = useCallback(() => { router.setQuery(defaultQuery, defaultQuery) }, [router, defaultQuery]) @@ -47,7 +47,7 @@ export const useQuery = (router: Router, defaultQuery: T) => { return { query, - setQuery: (query: Partial) => { + setQuery: (query: Query) => { router.setQuery(query, defaultQuery) }, resetQuery, @@ -81,7 +81,7 @@ export const useAbstractQuery = < >( router: Router, parseQuery: ParseQuery, - defaultQuery: TDefaultQuery, + defaultQuery: TDefaultQuery = {} as TDefaultQuery, options?: Partial, ) => { const mergedOptions = useMemo( diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts index 2a435e4..4f9a5b6 100644 --- a/src/routers/__test__/nextRouter.test.ts +++ b/src/routers/__test__/nextRouter.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react' import router from 'next/router' import { z } from 'zod' -import { vi } from 'vitest' +import { expectTypeOf, vi } from 'vitest' import { useQuery, useQueryAndPagination } from '../../zod/routers/nextRouter' import { usePagination } from '../nextRouter' @@ -82,11 +82,68 @@ describe('NextRouter', () => { }) expect(result.current.query.search).toBe(search) + expectTypeOf(result.current.query.search).toEqualTypeOf() expect(router.query.search).toBe(search) expect(result.current.page).toBe(0) expect(router.query.page).toBeUndefined() }) + test('passing no default query should return query values that are possibly undefined', () => { + const { result: resultQueryAndPagination } = renderHook(() => + useQueryAndPagination(searchSchema), + ) + expectTypeOf(resultQueryAndPagination.current.query.search).toEqualTypeOf< + string | undefined + >() + + const { result: resultQuery } = renderHook(() => useQuery(searchSchema)) + expectTypeOf(resultQuery.current.query.search).toEqualTypeOf< + string | undefined + >() + }) + + test('passing a default query should return query values that are not undefined', () => { + const { result: resultQueryAndPagination } = renderHook(() => + useQueryAndPagination(searchSchema, { search: '' }), + ) + expectTypeOf( + resultQueryAndPagination.current.query.search, + ).toEqualTypeOf() + + const { result: resultQuery } = renderHook(() => + useQuery(searchSchema, { search: '' }), + ) + expectTypeOf(resultQuery.current.query.search).toEqualTypeOf() + + const { result: resultPagination } = renderHook(() => usePagination()) + expectTypeOf(resultPagination.current.page).toEqualTypeOf() + }) + + test('passing a partial default query should return the default query values as not undefined and the remaining query values as possibly undefined', () => { + const querySchema = z.object({ + role: z.string(), + name: z.string(), + }) + + const { result: resultQueryAndPagination } = renderHook(() => + useQueryAndPagination(querySchema, { role: '' }), + ) + expectTypeOf( + resultQueryAndPagination.current.query.role, + ).toEqualTypeOf() + expectTypeOf(resultQueryAndPagination.current.query.name).toEqualTypeOf< + string | undefined + >() + + const { result: resultQuery } = renderHook(() => + useQuery(querySchema, { name: '' }), + ) + expectTypeOf(resultQuery.current.query.role).toEqualTypeOf< + string | undefined + >() + expectTypeOf(resultQuery.current.query.name).toEqualTypeOf() + }) + test('change default parameters', () => { const page = 1 const size = 10 @@ -261,6 +318,7 @@ describe('NextRouter', () => { }) expect(result.current.query.search).toBe(search) + expectTypeOf(result.current.query.search).toEqualTypeOf() expect(router.query.search).toBe(search) }) @@ -270,6 +328,9 @@ describe('NextRouter', () => { ) expect(result.current.query.department).toBeUndefined() + expectTypeOf(result.current.query.department).toEqualTypeOf< + string | undefined + >() }) test('the query should be a merge of the default query and the current query', () => { diff --git a/src/routers/inMemory.ts b/src/routers/inMemory.ts index 9384900..7d3af3e 100644 --- a/src/routers/inMemory.ts +++ b/src/routers/inMemory.ts @@ -29,18 +29,24 @@ const useInMemoryRouter = (): Router => { } } -export const useQuery = ( +export const useQuery = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( parseQuery: ParseQuery, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, options?: Partial, ) => { const router = useInMemoryRouter() return useAbstractQuery(router, parseQuery, defaultQuery, options) } -export const useQueryAndPagination = ( +export const useQueryAndPagination = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( parseQuery: ParseQuery, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, defaultPagination?: Partial, options?: Partial, ) => { diff --git a/src/routers/nextRouter.ts b/src/routers/nextRouter.ts index 50aaccf..ac4d69b 100644 --- a/src/routers/nextRouter.ts +++ b/src/routers/nextRouter.ts @@ -55,23 +55,29 @@ const useNextRouter = ( }) } }, - isReady: nextRouter.isReady, + isQueryReady: nextRouter.isReady, } } -export const useQuery = ( +export const useQuery = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( parseQuery: ParseQuery, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, options?: Partial, ) => { const router = useNextRouter(options) const result = useAbstractQuery(router, parseQuery, defaultQuery, options) - return { ...result, queryIsReady: router.isReady } + return { ...result, isQueryReady: router.isQueryReady } } -export const useQueryAndPagination = ( +export const useQueryAndPagination = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( parseQuery: ParseQuery, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, defaultPagination?: Partial, options?: Partial, ) => { @@ -83,7 +89,7 @@ export const useQueryAndPagination = ( defaultPagination, options, ) - return { ...result, queryIsReady: router.isReady } + return { ...result, isQueryReady: router.isQueryReady } } export const usePagination = ( @@ -97,7 +103,7 @@ export const usePagination = ( setSize, setPagination, resetPagination, - queryIsReady, + isQueryReady, } = useQueryAndPagination(() => ({}), {}, defaultPagination, options) return { page, @@ -106,6 +112,6 @@ export const usePagination = ( setSize, setPagination, resetPagination, - queryIsReady, + isQueryReady, } } diff --git a/src/routers/reactRouter.ts b/src/routers/reactRouter.ts index 17b8b25..9bc8235 100644 --- a/src/routers/reactRouter.ts +++ b/src/routers/reactRouter.ts @@ -67,18 +67,24 @@ const useReactRouter = ( } } -export const useQuery = ( +export const useQuery = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( parseQuery: ParseQuery, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, options?: Partial, ) => { const router = useReactRouter(options) return useAbstractQuery(router, parseQuery, defaultQuery, options) } -export const useQueryAndPagination = ( +export const useQueryAndPagination = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( parseQuery: ParseQuery, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, defaultPagination?: Partial, options?: Partial, ) => { diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts index 117cb5c..3755cec 100644 --- a/src/zod/routers/inMemory.ts +++ b/src/zod/routers/inMemory.ts @@ -13,7 +13,12 @@ export const useQuery = < schemaQuery: TSchema, defaultQuery: Partial = {}, options?: Partial, -) => useQueryVanilla(zodParser(schemaQuery), defaultQuery, options) +) => + useQueryVanilla( + zodParser(schemaQuery), + defaultQuery, + options, + ) export const useQueryAndPagination = < TSchema extends z.ZodTypeAny, @@ -25,7 +30,7 @@ export const useQueryAndPagination = < options?: Partial, ) => useQueryAndPaginationVanilla( - zodParser(schemaQuery), + zodParser(schemaQuery), defaultQuery, defaultPagination, options, diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index 46f3b46..dcf2351 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -10,23 +10,30 @@ import { RouterWithHistoryOptions } from '../../routers/shared' export const useQuery = < TSchema extends z.ZodTypeAny, TQuery extends z.output, + TDefaultQuery extends Partial, >( schemaQuery: TSchema, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, options?: Partial, -) => useQueryVanilla(zodParser(schemaQuery), defaultQuery, options) +) => + useQueryVanilla( + zodParser(schemaQuery), + defaultQuery, + options, + ) export const useQueryAndPagination = < TSchema extends z.ZodTypeAny, TQuery extends z.output, + TDefaultQuery extends Partial, >( schemaQuery: TSchema, - defaultQuery: Partial = {}, + defaultQuery?: TDefaultQuery, defaultPagination?: Partial, options?: Partial, ) => useQueryAndPaginationVanilla( - zodParser(schemaQuery), + zodParser(schemaQuery), defaultQuery, defaultPagination, options, diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index 634b947..8f3c10a 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -14,7 +14,12 @@ export const useQuery = < schemaQuery: TSchema, defaultQuery: Partial = {}, options?: Partial, -) => useQueryVanilla(zodParser(schemaQuery), defaultQuery, options) +) => + useQueryVanilla( + zodParser(schemaQuery), + defaultQuery, + options, + ) export const useQueryAndPagination = < TSchema extends z.ZodTypeAny, @@ -26,7 +31,7 @@ export const useQueryAndPagination = < options?: Partial, ) => useQueryAndPaginationVanilla( - zodParser(schemaQuery), + zodParser(schemaQuery), defaultQuery, defaultPagination, options,