Skip to content

Commit 13958ae

Browse files
authored
feat(use-dataloader): handle primitive array key (#920)
* feat(use-dataloader): handle primitive array key * fix(paginated-dataloader): useless memo
1 parent 72328fb commit 13958ae

File tree

7 files changed

+131
-61
lines changed

7 files changed

+131
-61
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { marshalQueryKey } from '../helpers'
2+
3+
describe('marshalQueryKey', () => {
4+
test('should accept a string', () => {
5+
expect(marshalQueryKey('string')).toStrictEqual('string')
6+
})
7+
8+
test('should accept a number', () => {
9+
expect(marshalQueryKey(0)).toStrictEqual('0')
10+
expect(marshalQueryKey(1)).toStrictEqual('1')
11+
})
12+
13+
test('should accept primitive array', () => {
14+
const date = new Date('2021')
15+
expect(marshalQueryKey(['defaultKey', 3, null, date, true])).toStrictEqual(
16+
'defaultKey.3.2021-01-01T00:00:00.000Z.true',
17+
)
18+
19+
expect(
20+
marshalQueryKey(
21+
[
22+
'default key',
23+
['number', 3],
24+
['null', null],
25+
['date', date],
26+
['boolean', true],
27+
].flat(),
28+
),
29+
).toStrictEqual(
30+
'default key.number.3.null.date.2021-01-01T00:00:00.000Z.boolean.true',
31+
)
32+
})
33+
34+
test('should not accept object', () => {
35+
// @ts-expect-error used because we test with bad key
36+
expect(marshalQueryKey(['default key', { object: 'no' }])).toStrictEqual(
37+
'default key.[object Object]',
38+
)
39+
})
40+
})

packages/use-dataloader/src/__tests__/useDataLoader.test.tsx

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
import { renderHook, waitFor } from '@testing-library/react'
33
import { ReactNode } from 'react'
44
import DataLoaderProvider, { useDataLoaderContext } from '../DataLoaderProvider'
5-
import { KEY_IS_NOT_STRING_ERROR } from '../constants'
6-
import { UseDataLoaderConfig } from '../types'
5+
import { KeyType, UseDataLoaderConfig } from '../types'
76
import useDataLoader from '../useDataLoader'
87

98
type UseDataLoaderHookProps = {
109
config: UseDataLoaderConfig<unknown, unknown>
11-
key: string
10+
key: KeyType
1211
method: () => Promise<unknown>
1312
children?: ReactNode
1413
}
@@ -61,6 +60,52 @@ describe('useDataLoader', () => {
6160
expect(result.current.previousData).toBe(undefined)
6261
})
6362

63+
test('should render correctly with a complexe key', async () => {
64+
const key = [
65+
'baseKey',
66+
['null', null],
67+
['boolean', false],
68+
['number', 10],
69+
].flat()
70+
71+
const method = jest.fn(
72+
() =>
73+
new Promise(resolve => {
74+
setTimeout(() => resolve(true), PROMISE_TIMEOUT)
75+
}),
76+
)
77+
78+
const initProps = {
79+
...initialProps,
80+
key,
81+
method,
82+
}
83+
84+
const { result, rerender } = renderHook(
85+
props => useDataLoader(props.key, props.method),
86+
{
87+
initialProps: initProps,
88+
wrapper,
89+
},
90+
)
91+
expect(result.current.data).toBe(undefined)
92+
expect(result.current.isLoading).toBe(true)
93+
expect(result.current.previousData).toBe(undefined)
94+
expect(initialProps.method).toBeCalledTimes(1)
95+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
96+
expect(initialProps.method).toBeCalledTimes(1)
97+
expect(result.current.data).toBe(true)
98+
expect(result.current.isLoading).toBe(false)
99+
expect(result.current.previousData).toBe(undefined)
100+
101+
rerender({ ...initProps })
102+
103+
expect(initialProps.method).toBeCalledTimes(1)
104+
expect(result.current.data).toBe(true)
105+
expect(result.current.isLoading).toBe(false)
106+
expect(result.current.previousData).toBe(undefined)
107+
})
108+
64109
test('should render correctly without request enabled then enable it', async () => {
65110
const method = jest.fn(
66111
() =>
@@ -96,28 +141,6 @@ describe('useDataLoader', () => {
96141
expect(result.current.data).toBe(true)
97142
})
98143

99-
test('should render correctly without valid key', () => {
100-
const orignalConsoleError = console.error
101-
console.error = jest.fn
102-
try {
103-
renderHook(
104-
// @ts-expect-error used because we test with bad key
105-
props => useDataLoader(props.key, props.method),
106-
{
107-
initialProps: {
108-
...initialProps,
109-
key: 2,
110-
},
111-
wrapper,
112-
},
113-
)
114-
fail('It shoulded fail with a bad key')
115-
} catch (error) {
116-
expect((error as Error)?.message).toBe(KEY_IS_NOT_STRING_ERROR)
117-
}
118-
console.error = orignalConsoleError
119-
})
120-
121144
test('should render correctly without keepPreviousData', async () => {
122145
const { result } = renderHook(
123146
props => useDataLoader(props.key, props.method, props.config),

packages/use-dataloader/src/__tests__/usePaginatedDataLoader.test.tsx

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { act, renderHook, waitFor } from '@testing-library/react'
33
import { ReactNode } from 'react'
44
import DataLoaderProvider from '../DataLoaderProvider'
5-
import { KEY_IS_NOT_STRING_ERROR } from '../constants'
65
import { UsePaginatedDataLoaderMethodParams } from '../types'
76
import usePaginatedDataLoader from '../usePaginatedDataLoader'
87

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

83-
test('should render correctly without valid key', () => {
84-
const orignalConsoleError = console.error
85-
console.error = jest.fn
86-
try {
87-
renderHook(
88-
// @ts-expect-error used because we test with bad key
89-
props => usePaginatedDataLoader(props.key, props.method),
90-
{
91-
initialProps: {
92-
...initialProps,
93-
key: 2,
94-
},
95-
wrapper,
96-
},
97-
)
98-
fail('It shoulded fail with a bad key')
99-
} catch (error) {
100-
expect((error as Error)?.message).toBe(KEY_IS_NOT_STRING_ERROR)
101-
}
102-
console.error = orignalConsoleError
103-
})
104-
10582
test('should render correctly with result null', async () => {
10683
const { result } = renderHook(
10784
props => usePaginatedDataLoader(props.key, props.method, props.config),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { KeyType } from './types'
2+
3+
/**
4+
*
5+
* @param {KeyType} queryKey
6+
* @returns string
7+
*/
8+
export const marshalQueryKey = (queryKey: KeyType) =>
9+
Array.isArray(queryKey)
10+
? queryKey
11+
.filter(Boolean)
12+
.map(subKey => {
13+
if (subKey instanceof Date) {
14+
return subKey.toISOString()
15+
}
16+
17+
return subKey?.toString()
18+
})
19+
.join('.')
20+
: queryKey?.toString()

packages/use-dataloader/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
type PrimitiveType = string | number | boolean | null | undefined | Date
2+
export type KeyType = string | number | PrimitiveType[]
3+
14
export class PromiseType<T = unknown> extends Promise<T> {
25
public cancel?: () => void
36
}

packages/use-dataloader/src/useDataLoader.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { useDataLoaderContext } from './DataLoaderProvider'
33
import { StatusEnum } from './constants'
4-
import { PromiseType, UseDataLoaderConfig, UseDataLoaderResult } from './types'
4+
import { marshalQueryKey } from './helpers'
5+
import {
6+
KeyType,
7+
PromiseType,
8+
UseDataLoaderConfig,
9+
UseDataLoaderResult,
10+
} from './types'
511

612
function useDataLoader<ResultType = unknown, ErrorType = Error>(
7-
fetchKey: string,
13+
key: KeyType,
814
method: () => PromiseType<ResultType>,
915
{
1016
enabled = true,
@@ -23,11 +29,14 @@ function useDataLoader<ResultType = unknown, ErrorType = Error>(
2329
const onErrorRef = useRef(onError ?? onGlobalError)
2430
const needPollingRef = useRef(needPolling)
2531
const [, setCounter] = useState(0)
32+
2633
const forceRerender = useCallback(() => {
2734
setCounter(current => current + 1)
2835
}, [])
2936

30-
const request = getOrAddRequest<ResultType, ErrorType>(fetchKey, {
37+
const queryKey = useMemo(() => marshalQueryKey(key), [key])
38+
39+
const request = getOrAddRequest<ResultType, ErrorType>(queryKey, {
3140
enabled,
3241
method: methodRef.current,
3342
})

packages/use-dataloader/src/usePaginatedDataLoader.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useCallback, useEffect, useState } from 'react'
2-
import { KEY_IS_NOT_STRING_ERROR } from './constants'
1+
import { useCallback, useEffect, useMemo, useState } from 'react'
32
import {
3+
KeyType,
44
PromiseType,
55
UsePaginatedDataLoaderConfig,
66
UsePaginatedDataLoaderMethodParams,
@@ -9,13 +9,13 @@ import {
99
import useDataLoader from './useDataLoader'
1010

1111
/**
12-
* @param {string} baseFetchKey base key used to cache data. Hook append -page-X to that key for each page you load
12+
* @param {KeyType} key base key used to cache data. Hook append -page-X to that key for each page you load
1313
* @param {() => PromiseType} method a method that return a promise
1414
* @param {useDataLoaderConfig} config hook configuration
1515
* @returns {useDataLoaderResult} hook result containing data, request state, and method to reload the data
1616
*/
1717
const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
18-
baseFetchKey: string,
18+
key: KeyType,
1919
method: (
2020
params: UsePaginatedDataLoaderMethodParams,
2121
) => PromiseType<ResultType>,
@@ -32,13 +32,11 @@ const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
3232
perPage = 1,
3333
}: UsePaginatedDataLoaderConfig<ResultType, ErrorType> = {},
3434
): UsePaginatedDataLoaderResult<ResultType, ErrorType> => {
35-
if (typeof baseFetchKey !== 'string') {
36-
throw new Error(KEY_IS_NOT_STRING_ERROR)
37-
}
38-
3935
const [data, setData] = useState<Record<number, ResultType | undefined>>({})
4036
const [page, setPage] = useState<number>(initialPage ?? 1)
4137

38+
const keyPage = useMemo(() => [key, ['page', page]].flat(), [key, page])
39+
4240
const pageMethod = useCallback(
4341
() => method({ page, perPage }),
4442
[method, page, perPage],
@@ -52,7 +50,7 @@ const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
5250
isSuccess,
5351
reload,
5452
error,
55-
} = useDataLoader(`${baseFetchKey}-page-${page}`, pageMethod, {
53+
} = useDataLoader(keyPage, pageMethod, {
5654
dataLifetime,
5755
enabled,
5856
initialData,
@@ -88,7 +86,7 @@ const usePaginatedDataLoader = <ResultType = unknown, ErrorType = Error>(
8886
useEffect(() => {
8987
setPage(1)
9088
setData({})
91-
}, [baseFetchKey])
89+
}, [key])
9290

9391
return {
9492
data,

0 commit comments

Comments
 (0)