Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/use-dataloader/src/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { marshalQueryKey } from '../helpers'

describe('marshalQueryKey', () => {
test('should accept a string', () => {
expect(marshalQueryKey('string')).toStrictEqual('string')
})

test('should accept a number', () => {
expect(marshalQueryKey(0)).toStrictEqual('0')
expect(marshalQueryKey(1)).toStrictEqual('1')
})

test('should accept primitive array', () => {
const date = new Date('2021')
expect(marshalQueryKey(['defaultKey', 3, null, date, true])).toStrictEqual(
'defaultKey.3.2021-01-01T00:00:00.000Z.true',
)

expect(
marshalQueryKey(
[
'default key',
['number', 3],
['null', null],
['date', date],
['boolean', true],
].flat(),
),
).toStrictEqual(
'default key.number.3.null.date.2021-01-01T00:00:00.000Z.boolean.true',
)
})

test('should not accept object', () => {
// @ts-expect-error used because we test with bad key
expect(marshalQueryKey(['default key', { object: 'no' }])).toStrictEqual(
'default key.[object Object]',
)
})
})
73 changes: 48 additions & 25 deletions packages/use-dataloader/src/__tests__/useDataLoader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
import { renderHook, waitFor } from '@testing-library/react'
import { ReactNode } from 'react'
import DataLoaderProvider, { useDataLoaderContext } from '../DataLoaderProvider'
import { KEY_IS_NOT_STRING_ERROR } from '../constants'
import { UseDataLoaderConfig } from '../types'
import { KeyType, UseDataLoaderConfig } from '../types'
import useDataLoader from '../useDataLoader'

type UseDataLoaderHookProps = {
config: UseDataLoaderConfig<unknown, unknown>
key: string
key: KeyType
method: () => Promise<unknown>
children?: ReactNode
}
Expand Down Expand Up @@ -61,6 +60,52 @@ describe('useDataLoader', () => {
expect(result.current.previousData).toBe(undefined)
})

test('should render correctly with a complexe key', async () => {
const key = [
'baseKey',
['null', null],
['boolean', false],
['number', 10],
].flat()

const method = jest.fn(
() =>
new Promise(resolve => {
setTimeout(() => resolve(true), PROMISE_TIMEOUT)
}),
)

const initProps = {
...initialProps,
key,
method,
}

const { result, rerender } = renderHook(
props => useDataLoader(props.key, props.method),
{
initialProps: initProps,
wrapper,
},
)
expect(result.current.data).toBe(undefined)
expect(result.current.isLoading).toBe(true)
expect(result.current.previousData).toBe(undefined)
expect(initialProps.method).toBeCalledTimes(1)
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(initialProps.method).toBeCalledTimes(1)
expect(result.current.data).toBe(true)
expect(result.current.isLoading).toBe(false)
expect(result.current.previousData).toBe(undefined)

rerender({ ...initProps })

expect(initialProps.method).toBeCalledTimes(1)
expect(result.current.data).toBe(true)
expect(result.current.isLoading).toBe(false)
expect(result.current.previousData).toBe(undefined)
})

test('should render correctly without request enabled then enable it', async () => {
const method = jest.fn(
() =>
Expand Down Expand Up @@ -96,28 +141,6 @@ describe('useDataLoader', () => {
expect(result.current.data).toBe(true)
})

test('should render correctly without valid key', () => {
const orignalConsoleError = console.error
console.error = jest.fn
try {
renderHook(
// @ts-expect-error used because we test with bad key
props => useDataLoader(props.key, props.method),
{
initialProps: {
...initialProps,
key: 2,
},
wrapper,
},
)
fail('It shoulded fail with a bad key')
} catch (error) {
expect((error as Error)?.message).toBe(KEY_IS_NOT_STRING_ERROR)
}
console.error = orignalConsoleError
})

test('should render correctly without keepPreviousData', async () => {
const { result } = renderHook(
props => useDataLoader(props.key, props.method, props.config),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { act, renderHook, waitFor } from '@testing-library/react'
import { ReactNode } from 'react'
import DataLoaderProvider from '../DataLoaderProvider'
import { KEY_IS_NOT_STRING_ERROR } from '../constants'
import { UsePaginatedDataLoaderMethodParams } from '../types'
import usePaginatedDataLoader from '../usePaginatedDataLoader'

Expand Down Expand Up @@ -80,28 +79,6 @@ describe('usePaginatedDataLoader', () => {
expect(result.current.pageData).toBe(true)
})

test('should render correctly without valid key', () => {
const orignalConsoleError = console.error
console.error = jest.fn
try {
renderHook(
// @ts-expect-error used because we test with bad key
props => usePaginatedDataLoader(props.key, props.method),
{
initialProps: {
...initialProps,
key: 2,
},
wrapper,
},
)
fail('It shoulded fail with a bad key')
} catch (error) {
expect((error as Error)?.message).toBe(KEY_IS_NOT_STRING_ERROR)
}
console.error = orignalConsoleError
})

test('should render correctly with result null', async () => {
const { result } = renderHook(
props => usePaginatedDataLoader(props.key, props.method, props.config),
Expand Down
20 changes: 20 additions & 0 deletions packages/use-dataloader/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { KeyType } from './types'

/**
*
* @param {KeyType} queryKey
* @returns string
*/
export const marshalQueryKey = (queryKey: KeyType) =>
Array.isArray(queryKey)
? queryKey
.filter(Boolean)
.map(subKey => {
if (subKey instanceof Date) {
return subKey.toISOString()
}

return subKey?.toString()
})
.join('.')
: queryKey?.toString()
3 changes: 3 additions & 0 deletions packages/use-dataloader/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
type PrimitiveType = string | number | boolean | null | undefined | Date
export type KeyType = string | number | PrimitiveType[]

export class PromiseType<T = unknown> extends Promise<T> {
public cancel?: () => void
}
Expand Down
15 changes: 12 additions & 3 deletions packages/use-dataloader/src/useDataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useDataLoaderContext } from './DataLoaderProvider'
import { StatusEnum } from './constants'
import { PromiseType, UseDataLoaderConfig, UseDataLoaderResult } from './types'
import { marshalQueryKey } from './helpers'
import {
KeyType,
PromiseType,
UseDataLoaderConfig,
UseDataLoaderResult,
} from './types'

function useDataLoader<ResultType = unknown, ErrorType = Error>(
fetchKey: string,
key: KeyType,
method: () => PromiseType<ResultType>,
{
enabled = true,
Expand All @@ -23,11 +29,14 @@ function useDataLoader<ResultType = unknown, ErrorType = Error>(
const onErrorRef = useRef(onError ?? onGlobalError)
const needPollingRef = useRef(needPolling)
const [, setCounter] = useState(0)

const forceRerender = useCallback(() => {
setCounter(current => current + 1)
}, [])

const request = getOrAddRequest<ResultType, ErrorType>(fetchKey, {
const queryKey = useMemo(() => marshalQueryKey(key), [key])

const request = getOrAddRequest<ResultType, ErrorType>(queryKey, {
enabled,
method: methodRef.current,
})
Expand Down
18 changes: 8 additions & 10 deletions packages/use-dataloader/src/usePaginatedDataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
import { KEY_IS_NOT_STRING_ERROR } from './constants'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
KeyType,
PromiseType,
UsePaginatedDataLoaderConfig,
UsePaginatedDataLoaderMethodParams,
Expand All @@ -9,13 +9,13 @@ import {
import useDataLoader from './useDataLoader'

/**
* @param {string} baseFetchKey base key used to cache data. Hook append -page-X to that key for each page you load
* @param {KeyType} key base key used to cache data. Hook append -page-X to that key for each page you load
* @param {() => PromiseType} method a method that return a promise
* @param {useDataLoaderConfig} config hook configuration
* @returns {useDataLoaderResult} hook result containing data, request state, and method to reload the data
*/
const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
baseFetchKey: string,
key: KeyType,
method: (
params: UsePaginatedDataLoaderMethodParams,
) => PromiseType<ResultType>,
Expand All @@ -32,13 +32,11 @@ const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
perPage = 1,
}: UsePaginatedDataLoaderConfig<ResultType, ErrorType> = {},
): UsePaginatedDataLoaderResult<ResultType, ErrorType> => {
if (typeof baseFetchKey !== 'string') {
throw new Error(KEY_IS_NOT_STRING_ERROR)
}

const [data, setData] = useState<Record<number, ResultType | undefined>>({})
const [page, setPage] = useState<number>(initialPage ?? 1)

const keyPage = useMemo(() => [key, ['page', page]].flat(), [key, page])

const pageMethod = useCallback(
() => method({ page, perPage }),
[method, page, perPage],
Expand All @@ -52,7 +50,7 @@ const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
isSuccess,
reload,
error,
} = useDataLoader(`${baseFetchKey}-page-${page}`, pageMethod, {
} = useDataLoader(keyPage, pageMethod, {
dataLifetime,
enabled,
initialData,
Expand Down Expand Up @@ -88,7 +86,7 @@ const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
useEffect(() => {
setPage(1)
setData({})
}, [baseFetchKey])
}, [key])

return {
data,
Expand Down