diff --git a/readme.md b/readme.md index 596f32f..a6ea959 100644 --- a/readme.md +++ b/readme.md @@ -58,7 +58,7 @@ const parseSearch = (query: Query) => { export function UserList() { const { page, size, query, setQuery, setPage, resetQuery } = - useQueryAndPagination({ search: '' }, parseSearch) + useQueryAndPagination(parseSearch, { search: '' }) return (
@@ -116,10 +116,14 @@ const users = [ export function UserList() { const { page, size, query, setQuery, setPage, resetQuery } = - useQueryAndPagination({ name: '', age: 0 }, userSchema, { - page: 0, - size: 4, - }) + useQueryAndPagination: + userSchema, + { name: '', age: 0 }, + { + page: 0, + size: 4, + }, + ) return (
diff --git a/src/engine/pagination.ts b/src/engine/pagination.ts index c462789..4ba0a3e 100644 --- a/src/engine/pagination.ts +++ b/src/engine/pagination.ts @@ -46,11 +46,14 @@ const parsePagination = (query: Query): Partial => { } } -export const useAbstractQueryAndPagination = ( - defaultQuery: T, - parseQuery: ParseQuery, +export const useAbstractQueryAndPagination = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( router: Router, - defaultPagination?: PaginationQuery, + parseQuery: ParseQuery, + defaultQuery: TDefaultQuery, + defaultPagination?: Partial, options?: Partial, ) => { const mergedDefaultPagination = { @@ -63,9 +66,9 @@ export const useAbstractQueryAndPagination = ( } const { query, setQuery } = useAbstractQuery( - mergedDefaultQueryAndPagination, - parseQuery, router, + parseQuery, + mergedDefaultQueryAndPagination, options, ) @@ -74,15 +77,18 @@ export const useAbstractQueryAndPagination = ( setQuery: setPagination, resetQuery: resetPagination, } = useAbstractQuery( - mergedDefaultPagination, - parsePagination, router, + parsePagination, + mergedDefaultPagination, options, ) return { query, - setQuery: (query: Partial, options?: Partial) => { + setQuery: ( + query: Partial, + options?: Partial, + ) => { const mergedOptions = { ...DEFAULT_CHANGE_QUERY_OPTIONS, ...options } setQuery({ ...query, diff --git a/src/engine/query.ts b/src/engine/query.ts index e5f73f4..31b7bf3 100644 --- a/src/engine/query.ts +++ b/src/engine/query.ts @@ -24,17 +24,17 @@ export type ParseQuery = (query: Query) => Partial export type Router = { /** - * @returns The current query. + * @returns The current query. If a property is undefined by the current query, the corresponding property of the default query is taken, which might be undefined too. */ getQuery: (defaultQuery: Query) => Query /** - * Updates the query by merging the given query with the current query. + * Updates the query by merging the given query with the current query. If a property is undefined by the given query and the current query, the corresponding default query is taken, which might be undefined too. */ setQuery: (query: Partial, defaultQuery: Query) => void } -export const useQuery = (defaultQuery: T, router: Router) => { +export const useQuery = (router: Router, defaultQuery: T) => { const resetQuery = useCallback(() => { router.setQuery(defaultQuery, defaultQuery) }, [router, defaultQuery]) @@ -71,10 +71,13 @@ const DEFAULT_ABSTRACT_QUERY_OPTIONS: AbstractQueryOptions = { }, } -export const useAbstractQuery = ( - defaultQuery: T, - parseQuery: ParseQuery, +export const useAbstractQuery = < + TQuery extends AbstractQuery, + TDefaultQuery extends Partial, +>( router: Router, + parseQuery: ParseQuery, + defaultQuery: TDefaultQuery, options?: Partial, ) => { const mergedOptions = useMemo( @@ -88,12 +91,12 @@ export const useAbstractQuery = ( ) const { query, setQuery, resetQuery } = useQuery( - convertedDefaultQuery, router, + convertedDefaultQuery, ) - const parsedQuery: T = useMemo(() => { - let parsed: Partial + const parsedQuery = useMemo(() => { + let parsed: Partial try { parsed = parseQuery(query) } catch (e) { @@ -106,7 +109,7 @@ export const useAbstractQuery = ( }, [defaultQuery, parseQuery, query]) const setAbstractQuery = useCallback( - (query: Partial) => { + (query: Partial) => { setQuery(mergedOptions.convertToQuery(query)) }, [setQuery, mergedOptions], diff --git a/src/routers/__test__/inMemory.test.ts b/src/routers/__test__/inMemory.test.ts index eae893e..df0ccb6 100644 --- a/src/routers/__test__/inMemory.test.ts +++ b/src/routers/__test__/inMemory.test.ts @@ -1,6 +1,5 @@ import { act, renderHook } from '@testing-library/react' import { z } from 'zod' -import { NonNullableRecord } from '../../utils' import { useQuery, useQueryAndPagination } from '../../zod/routers/inMemory' import { usePagination } from '../inMemory' @@ -10,8 +9,8 @@ describe('InMemory', () => { }) const useInMemoryQueryWithSearch = ( - defaultQuery: NonNullableRecord>, - ) => useQuery(defaultQuery, searchSchema) + defaultQuery: Partial> = {}, + ) => useQuery(searchSchema, defaultQuery) test('should set default page and size', () => { const page = 0 @@ -53,7 +52,7 @@ describe('InMemory', () => { const page = 2 const { result } = renderHook(() => - useQueryAndPagination({ search: '' }, searchSchema), + useQueryAndPagination(searchSchema, { search: '' }), ) act(() => { @@ -90,7 +89,7 @@ describe('InMemory', () => { const page = 2 const { result } = renderHook(() => - useQueryAndPagination({ search: defaultSearch }, searchSchema), + useQueryAndPagination(searchSchema, { search: defaultSearch }), ) act(() => { @@ -125,7 +124,7 @@ describe('InMemory', () => { const page = 2 const { result } = renderHook(() => - useQueryAndPagination({ search: defaultSearch }, searchSchema), + useQueryAndPagination(searchSchema, { search: defaultSearch }), ) act(() => { @@ -159,7 +158,7 @@ describe('InMemory', () => { }) const { result } = renderHook(() => - useQuery({ search: '', department: '' }, schema), + useQuery(schema, { search: '', department: '' }), ) act(() => { @@ -178,7 +177,7 @@ describe('InMemory', () => { const search = '' const { result } = renderHook(() => - useQuery({ search: 'Default search' }, searchSchema), + useQuery(searchSchema, { search: 'Default search' }), ) act(() => { diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts index 6fb5b19..2a435e4 100644 --- a/src/routers/__test__/nextRouter.test.ts +++ b/src/routers/__test__/nextRouter.test.ts @@ -2,7 +2,6 @@ import { act, renderHook } from '@testing-library/react' import router from 'next/router' import { z } from 'zod' import { vi } from 'vitest' -import { NonNullableRecord } from '../../utils' import { useQuery, useQueryAndPagination } from '../../zod/routers/nextRouter' import { usePagination } from '../nextRouter' @@ -19,8 +18,8 @@ describe('NextRouter', () => { }) const useNextRouterQueryWithSearch = ( - defaultQuery: NonNullableRecord>, - ) => useQuery(defaultQuery, searchSchema) + defaultQuery: Partial> = {}, + ) => useQuery(searchSchema, defaultQuery) test('should set default page and size', () => { const page = 0 @@ -64,7 +63,7 @@ describe('NextRouter', () => { const page = 2 const { result } = renderHook(() => - useQueryAndPagination({ search: '' }, searchSchema), + useQueryAndPagination(searchSchema, { search: '' }), ) act(() => { @@ -104,7 +103,7 @@ describe('NextRouter', () => { const page = 2 const { result } = renderHook(() => - useQueryAndPagination({ search: defaultSearch }, searchSchema), + useQueryAndPagination(searchSchema, { search: defaultSearch }), ) act(() => { @@ -146,7 +145,7 @@ describe('NextRouter', () => { const page = 2 const { result } = renderHook(() => - useQueryAndPagination({ search: defaultSearch }, searchSchema), + useQueryAndPagination(searchSchema, { search: defaultSearch }), ) act(() => { @@ -194,17 +193,14 @@ describe('NextRouter', () => { }) const { result } = renderHook(() => - useQuery( - { - search: '', - department: '', - age: defaultAge, - birthDate: defaultBirthDate, - netWorth: defaultNetWorth, - darkMode: defaultDarkMode, - }, - schema, - ), + useQuery(schema, { + search: '', + department: '', + age: defaultAge, + birthDate: defaultBirthDate, + netWorth: defaultNetWorth, + darkMode: defaultDarkMode, + }), ) act(() => { @@ -226,7 +222,7 @@ describe('NextRouter', () => { const greeting = 'hello' router.query = { greeting } - const { result } = renderHook(() => useQuery({ search: '' }, searchSchema)) + const { result } = renderHook(() => useQuery(searchSchema, { search: '' })) act(() => { result.current.setQuery({ search: 'Max' }) @@ -242,7 +238,7 @@ describe('NextRouter', () => { router.query = { search: 'Max' } const { result } = renderHook(() => - useQuery({ search: defaultSearch }, searchSchema), + useQuery(searchSchema, { search: defaultSearch }), ) act(() => { @@ -257,7 +253,7 @@ describe('NextRouter', () => { const search = '' const { result } = renderHook(() => - useQuery({ search: 'Default search' }, searchSchema), + useQuery(searchSchema, { search: 'Default search' }), ) act(() => { @@ -267,4 +263,25 @@ describe('NextRouter', () => { expect(result.current.query.search).toBe(search) expect(router.query.search).toBe(search) }) + + test('passing no default query should return undefined for a property that is not in the query', () => { + const { result } = renderHook(() => + useQuery(z.object({ department: z.string() })), + ) + + expect(result.current.query.department).toBeUndefined() + }) + + test('the query should be a merge of the default query and the current query', () => { + const defaultRole = 'ADMIN' + + const { result } = renderHook(() => + useQuery(z.object({ department: z.string(), role: z.string() }), { + role: defaultRole, + }), + ) + + expect(result.current.query.department).toBeUndefined() + expect(result.current.query.role).toBe(defaultRole) + }) }) diff --git a/src/routers/__test__/reactRouter.test.tsx b/src/routers/__test__/reactRouter.test.tsx index 803ff28..a10f039 100644 --- a/src/routers/__test__/reactRouter.test.tsx +++ b/src/routers/__test__/reactRouter.test.tsx @@ -1,7 +1,6 @@ import { act, renderHook } from '@testing-library/react' import { z } from 'zod' import { BrowserRouter } from 'react-router-dom' -import { NonNullableRecord } from '../../utils' import { useQuery, useQueryAndPagination } from '../../zod/routers/reactRouter' import { usePagination } from '../reactRouter' @@ -24,8 +23,8 @@ describe('ReactRouter', () => { }) const useReactRouterQueryWithSearch = ( - defaultQuery: NonNullableRecord>, - ) => useQuery(defaultQuery, searchSchema) + defaultQuery: Partial> = {}, + ) => useQuery(searchSchema, defaultQuery) test('should set default page and size', () => { const page = 0 @@ -71,7 +70,7 @@ describe('ReactRouter', () => { const page = 2 const { result } = renderHookWithContext(() => - useQueryAndPagination({ search: '' }, searchSchema), + useQueryAndPagination(searchSchema, { search: '' }), ) act(() => { @@ -111,7 +110,7 @@ describe('ReactRouter', () => { const page = 2 const { result } = renderHookWithContext(() => - useQueryAndPagination({ search: defaultSearch }, searchSchema), + useQueryAndPagination(searchSchema, { search: defaultSearch }), ) act(() => { @@ -149,7 +148,7 @@ describe('ReactRouter', () => { const page = 2 const { result } = renderHookWithContext(() => - useQueryAndPagination({ search: defaultSearch }, searchSchema), + useQueryAndPagination(searchSchema, { search: defaultSearch }), ) act(() => { @@ -186,7 +185,7 @@ describe('ReactRouter', () => { }) const { result } = renderHookWithContext(() => - useQuery({ search: '', department: '' }, schema), + useQuery(schema, { search: '', department: '' }), ) act(() => { @@ -207,7 +206,7 @@ describe('ReactRouter', () => { window.history.pushState({}, '', `/?greeting=${greeting}`) const { result } = renderHookWithContext(() => - useQuery({ search: '' }, searchSchema), + useQuery(searchSchema, { search: '' }), ) act(() => { @@ -226,7 +225,7 @@ describe('ReactRouter', () => { window.history.pushState({}, '', `/?search=Max`) const { result } = renderHookWithContext(() => - useQuery({ search: defaultSearch }, searchSchema), + useQuery(searchSchema, { search: defaultSearch }), ) act(() => { @@ -241,7 +240,7 @@ describe('ReactRouter', () => { const search = '' const { result } = renderHookWithContext(() => - useQuery({ search: 'Default search' }, searchSchema), + useQuery(searchSchema, { search: 'Default search' }), ) act(() => { diff --git a/src/routers/inMemory.ts b/src/routers/inMemory.ts index c8e8f54..9384900 100644 --- a/src/routers/inMemory.ts +++ b/src/routers/inMemory.ts @@ -29,36 +29,36 @@ const useInMemoryRouter = (): Router => { } } -export const useQuery = ( - defaultQuery: T, - parseQuery: ParseQuery, +export const useQuery = ( + parseQuery: ParseQuery, + defaultQuery: Partial = {}, options?: Partial, ) => { const router = useInMemoryRouter() - return useAbstractQuery(defaultQuery, parseQuery, router, options) + return useAbstractQuery(router, parseQuery, defaultQuery, options) } -export const useQueryAndPagination = ( - defaultQuery: T, - parseQuery: ParseQuery, - defaultPagination?: PaginationQuery, +export const useQueryAndPagination = ( + parseQuery: ParseQuery, + defaultQuery: Partial = {}, + defaultPagination?: Partial, options?: Partial, ) => { const router = useInMemoryRouter() return useAbstractQueryAndPagination( - defaultQuery, - parseQuery, router, + parseQuery, + defaultQuery, defaultPagination, options, ) } export const usePagination = ( - defaultPagination?: PaginationQuery, + defaultPagination?: Partial, options?: Partial, ) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useQueryAndPagination({}, () => ({}), defaultPagination, options) + useQueryAndPagination(() => ({}), {}, defaultPagination, options) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/routers/nextRouter.ts b/src/routers/nextRouter.ts index 85b3dcd..23143e5 100644 --- a/src/routers/nextRouter.ts +++ b/src/routers/nextRouter.ts @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Query, ParseQuery, @@ -22,16 +22,25 @@ const useNextRouter = ( options: undefined | Partial, ): Router => { const nextRouter = useRouter() + const [nextRouterQuery, setNextRouterQuery] = useState< + typeof nextRouter.query + >({}) const mergedOptions = useMemo( () => ({ ...DEFAULT_NEXT_ROUTER_OPTIONS, ...options }), [options], ) + useEffect(() => { + if (nextRouter.isReady) { + setNextRouterQuery(nextRouter.query) + } + }, [nextRouter.isReady, nextRouter.query]) + return { getQuery: (defaultQuery) => { const query: Query = {} - for (const [key, value] of Object.entries(nextRouter.query)) { + for (const [key, value] of Object.entries(nextRouterQuery)) { if (value !== undefined) { query[key] = value } @@ -39,7 +48,7 @@ const useNextRouter = ( return { ...defaultQuery, ...query } }, setQuery: (query, defaultQuery) => { - const newQuery = { ...nextRouter.query, ...query } + const newQuery = { ...nextRouterQuery, ...query } const newQueryWithoutDefaults = Object.fromEntries( Object.entries(newQuery).filter( ([key, value]) => value !== defaultQuery[key], @@ -58,36 +67,36 @@ const useNextRouter = ( } } -export const useQuery = ( - defaultQuery: T, - parseQuery: ParseQuery, +export const useQuery = ( + parseQuery: ParseQuery, + defaultQuery: Partial = {}, options?: Partial, ) => { const router = useNextRouter(options) - return useAbstractQuery(defaultQuery, parseQuery, router, options) + return useAbstractQuery(router, parseQuery, defaultQuery, options) } -export const useQueryAndPagination = ( - defaultQuery: T, - parseQuery: ParseQuery, - defaultPagination?: PaginationQuery, +export const useQueryAndPagination = ( + parseQuery: ParseQuery, + defaultQuery: Partial = {}, + defaultPagination?: Partial, options?: Partial, ) => { const router = useNextRouter(options) return useAbstractQueryAndPagination( - defaultQuery, - parseQuery, router, + parseQuery, + defaultQuery, defaultPagination, options, ) } export const usePagination = ( - defaultPagination?: PaginationQuery, + defaultPagination?: Partial, options?: Partial, ) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useQueryAndPagination({}, () => ({}), defaultPagination, options) + useQueryAndPagination(() => ({}), {}, defaultPagination, options) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/routers/reactRouter.ts b/src/routers/reactRouter.ts index 5aaeb0f..17b8b25 100644 --- a/src/routers/reactRouter.ts +++ b/src/routers/reactRouter.ts @@ -67,36 +67,36 @@ const useReactRouter = ( } } -export const useQuery = ( - defaultQuery: T, - parseQuery: ParseQuery, +export const useQuery = ( + parseQuery: ParseQuery, + defaultQuery: Partial = {}, options?: Partial, ) => { const router = useReactRouter(options) - return useAbstractQuery(defaultQuery, parseQuery, router, options) + return useAbstractQuery(router, parseQuery, defaultQuery, options) } -export const useQueryAndPagination = ( - defaultQuery: T, - parseQuery: ParseQuery, - defaultPagination?: PaginationQuery, +export const useQueryAndPagination = ( + parseQuery: ParseQuery, + defaultQuery: Partial = {}, + defaultPagination?: Partial, options?: Partial, ) => { const router = useReactRouter(options) return useAbstractQueryAndPagination( - defaultQuery, - parseQuery, router, + parseQuery, + defaultQuery, defaultPagination, options, ) } export const usePagination = ( - defaultPagination?: PaginationQuery, + defaultPagination?: Partial, options?: Partial, ) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useQueryAndPagination({}, () => ({}), defaultPagination, options) + useQueryAndPagination(() => ({}), {}, defaultPagination, options) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts index 289b34f..117cb5c 100644 --- a/src/zod/routers/inMemory.ts +++ b/src/zod/routers/inMemory.ts @@ -4,24 +4,29 @@ import { useQuery as useQueryVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/inMemory' -import { NonNullableRecord } from '../../utils' import { zodParser } from '../util' -export const useQuery = ( - defaultQuery: NonNullableRecord>, +export const useQuery = < + TSchema extends z.ZodTypeAny, + TQuery extends z.output, +>( schemaQuery: TSchema, + defaultQuery: Partial = {}, options?: Partial, -) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) +) => useQueryVanilla(zodParser(schemaQuery), defaultQuery, options) -export const useQueryAndPagination = ( - defaultQuery: NonNullableRecord>, +export const useQueryAndPagination = < + TSchema extends z.ZodTypeAny, + TQuery extends z.output, +>( schemaQuery: TSchema, - defaultPagination?: PaginationQuery, + defaultQuery: Partial = {}, + defaultPagination?: Partial, options?: Partial, ) => useQueryAndPaginationVanilla( - defaultQuery, zodParser(schemaQuery), + defaultQuery, defaultPagination, options, ) diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index 3f26e28..46f3b46 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -6,23 +6,28 @@ import { } from '../../routers/nextRouter' import { zodParser } from '../util' import { RouterWithHistoryOptions } from '../../routers/shared' -import { NonNullableRecord } from '../../utils' -export const useQuery = ( - defaultQuery: NonNullableRecord>, +export const useQuery = < + TSchema extends z.ZodTypeAny, + TQuery extends z.output, +>( schemaQuery: TSchema, + defaultQuery: Partial = {}, options?: Partial, -) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) +) => useQueryVanilla(zodParser(schemaQuery), defaultQuery, options) -export const useQueryAndPagination = ( - defaultQuery: NonNullableRecord>, +export const useQueryAndPagination = < + TSchema extends z.ZodTypeAny, + TQuery extends z.output, +>( schemaQuery: TSchema, - defaultPagination?: PaginationQuery, + defaultQuery: Partial = {}, + defaultPagination?: Partial, options?: Partial, ) => useQueryAndPaginationVanilla( - defaultQuery, zodParser(schemaQuery), + defaultQuery, defaultPagination, options, ) diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index 0bbabe2..634b947 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -6,23 +6,28 @@ import { } from '../../routers/reactRouter' import { zodParser } from '../util' import { RouterWithHistoryOptions } from '../../routers/shared' -import { NonNullableRecord } from '../../utils' -export const useQuery = ( - defaultQuery: NonNullableRecord>, +export const useQuery = < + TSchema extends z.ZodTypeAny, + TQuery extends z.output, +>( schemaQuery: TSchema, + defaultQuery: Partial = {}, options?: Partial, -) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) +) => useQueryVanilla(zodParser(schemaQuery), defaultQuery, options) -export const useQueryAndPagination = ( - defaultQuery: NonNullableRecord>, +export const useQueryAndPagination = < + TSchema extends z.ZodTypeAny, + TQuery extends z.output, +>( schemaQuery: TSchema, - defaultPagination?: PaginationQuery, + defaultQuery: Partial = {}, + defaultPagination?: Partial, options?: Partial, ) => useQueryAndPaginationVanilla( - defaultQuery, zodParser(schemaQuery), + defaultQuery, defaultPagination, options, )