Skip to content

Commit

Permalink
add atomWithInfiniteQuery with existing tests adjusted to v5 api
Browse files Browse the repository at this point in the history
  • Loading branch information
kalijonn committed Dec 2, 2023
1 parent c1a064b commit 2207e4c
Show file tree
Hide file tree
Showing 4 changed files with 404 additions and 19 deletions.
365 changes: 365 additions & 0 deletions __tests__/atomWithInfiniteQuery_spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
import React, {
Component,
StrictMode,
Suspense,
useCallback,

Check warning on line 5 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'useCallback' is defined but never used. Allowed unused vars must match /^_/u
useEffect,

Check warning on line 6 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'useEffect' is defined but never used. Allowed unused vars must match /^_/u
} from 'react'
import type { ReactNode } from 'react'
import {
InfiniteData,

Check warning on line 10 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'InfiniteData' is defined but never used. Allowed unused vars must match /^_/u
QueryFunctionContext,

Check warning on line 11 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'QueryFunctionContext' is defined but never used. Allowed unused vars must match /^_/u
QueryKey,

Check warning on line 12 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'QueryKey' is defined but never used. Allowed unused vars must match /^_/u
} from '@tanstack/query-core'
import { fireEvent, render } from '@testing-library/react'
import { useAtom, useSetAtom } from 'jotai/react'

Check warning on line 15 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'useSetAtom' is defined but never used. Allowed unused vars must match /^_/u
import { Getter, atom } from 'jotai/vanilla'

Check warning on line 16 in __tests__/atomWithInfiniteQuery_spec.tsx

View workflow job for this annotation

GitHub Actions / test

'Getter' is defined but never used. Allowed unused vars must match /^_/u
import { atomWithInfiniteQuery } from '../src/index'

beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.runAllTimers()
jest.useRealTimers()
})

it('infinite query basic test', async () => {
let resolve = () => {}
type DataResponse = { response: { count: number } }
const countAtom = atomWithInfiniteQuery<DataResponse>(() => ({
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.response.count + 1,
queryKey: ['countInfinite'],

queryFn: async ({ pageParam }) => {
await new Promise<void>((r) => (resolve = r))
return { response: { count: pageParam as number } }
},
}))

const Counter = () => {
const [countData] = useAtom(countAtom)

const { data, isPending, isError } = countData

if (isPending) return <>loading</>
if (isError) return <>error</>

return (
<>
<div>page count: {data.pages.length}</div>
</>
)
}

const { findByText } = render(
<StrictMode>
<Counter />
</StrictMode>
)

await findByText('loading')
resolve()
await findByText('page count: 1')
})

it('infinite query next page test', async () => {
const mockFetch = jest.fn((response) => ({ response }))
let resolve = () => {}
const countAtom = atomWithInfiniteQuery<{ response: { count: number } }>(
() => ({
initialPageParam: 1,
queryKey: ['nextPageAtom'],
queryFn: async ({ pageParam }) => {
await new Promise<void>((r) => (resolve = r))
return mockFetch({ count: pageParam as number })
},
getNextPageParam: (lastPage) => {
const {
response: { count },
} = lastPage
return (count + 1).toString()
},
getPreviousPageParam: (lastPage) => {
const {
response: { count },
} = lastPage
return (count - 1).toString()
},
})
)
const Counter = () => {
const [countData] = useAtom(countAtom)

const { isPending, isError, data, fetchNextPage, fetchPreviousPage } =
countData

if (isPending) return <>loading</>
if (isError) return <>error</>

return (
<>
<div>page count: {data.pages.length}</div>
<button onClick={() => fetchNextPage()}>next</button>
<button onClick={() => fetchPreviousPage()}>prev</button>
</>
)
}

const { findByText, getByText } = render(
<>
<Counter />
</>
)

await findByText('loading')
resolve()
await findByText('page count: 1')
expect(mockFetch).toBeCalledTimes(1)

fireEvent.click(getByText('next'))
resolve()
await findByText('page count: 2')
expect(mockFetch).toBeCalledTimes(2)

fireEvent.click(getByText('prev'))
resolve()
await findByText('page count: 3')
expect(mockFetch).toBeCalledTimes(3)
})

it('infinite query with enabled', async () => {
const slugAtom = atom<string | null>(null)

let resolve = () => {}
type DataResponse = {
response: {
slug: string
currentPage: number
}
}
const slugQueryAtom = atomWithInfiniteQuery<DataResponse>((get) => {
const slug = get(slugAtom)
return {
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.response.currentPage + 1,
enabled: !!slug,
queryKey: ['disabled_until_value', slug],
queryFn: async ({ pageParam }) => {
await new Promise<void>((r) => (resolve = r))
return {
response: { slug: `hello-${slug}`, currentPage: pageParam as number },
}
},
}
})

const Slug = () => {
const [slugQueryData] = useAtom(slugQueryAtom)
const { data, isPending, isError, fetchStatus } = slugQueryData

if (isPending && fetchStatus === 'idle') return <div>not enabled</div>

if (isPending) return <>loading</>
if (isError) return <>error</>

return <div>slug: {data?.pages?.[0]?.response?.slug}</div>
}

const Parent = () => {
const [, setSlug] = useAtom(slugAtom)
return (
<div>
<button
onClick={() => {
setSlug('world')
}}>
set slug
</button>
<Slug />
</div>
)
}

const { getByText, findByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Parent />
</Suspense>
</StrictMode>
)

await findByText('not enabled')

fireEvent.click(getByText('set slug'))
await findByText('loading')
resolve()
await findByText('slug: hello-world')
})

it('infinite query with enabled 2', async () => {
jest.useRealTimers() // FIXME can avoid?

const enabledAtom = atom<boolean>(true)
const slugAtom = atom<string | null>('first')
type DataResponse = {
response: {
slug: string
currentPage: number
}
}
const slugQueryAtom = atomWithInfiniteQuery<DataResponse>((get) => {
const slug = get(slugAtom)
const isEnabled = get(enabledAtom)
return {
getNextPageParam: (lastPage) => lastPage.response.currentPage + 1,
initialPageParam: 1,
enabled: isEnabled,
queryKey: ['enabled_toggle'],
queryFn: async ({ pageParam }) => {
await new Promise<void>((r) => setTimeout(r, 100)) // FIXME can avoid?
return {
response: { slug: `hello-${slug}`, currentPage: pageParam as number },
}
},
}
})

const Slug = () => {
const [slugQueryData] = useAtom(slugQueryAtom)
const { data, isPending, isError, fetchStatus } = slugQueryData

if (isPending && fetchStatus === 'idle') return <div>not enabled</div>

if (isPending) return <>loading</>
if (isError) return <>error</>

return <div>slug: {data.pages[0]?.response.slug}</div>
}

const Parent = () => {
const [, setSlug] = useAtom(slugAtom)
const [, setEnabled] = useAtom(enabledAtom)
return (
<div>
<button
onClick={() => {
setSlug('world')
}}>
set slug
</button>
<button
onClick={() => {
setEnabled(true)
}}>
set enabled
</button>
<button
onClick={() => {
setEnabled(false)
}}>
set disabled
</button>
<Slug />
</div>
)
}

const { getByText, findByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Parent />
</Suspense>
</StrictMode>
)

await findByText('loading')
await findByText('slug: hello-first')

await new Promise((r) => setTimeout(r, 100)) // FIXME we want to avoid this
fireEvent.click(getByText('set disabled'))
fireEvent.click(getByText('set slug'))

await new Promise((r) => setTimeout(r, 100)) // FIXME we want to avoid this
await findByText('slug: hello-first')

await new Promise((r) => setTimeout(r, 100)) // FIXME we want to avoid this
fireEvent.click(getByText('set enabled'))
await findByText('slug: hello-world')
})

describe('error handling', () => {
class ErrorBoundary extends Component<
{ message?: string; retry?: () => void; children: ReactNode },
{ hasError: boolean }
> {
constructor(props: { message?: string; children: ReactNode }) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
return this.state.hasError ? (
<div>
{this.props.message || 'errored'}
{this.props.retry && (
<button
onClick={() => {
this.props.retry?.()
this.setState({ hasError: false })
}}>
retry
</button>
)}
</div>
) : (
this.props.children
)
}
}

it('can catch error in error boundary', async () => {
let resolve = () => {}
const countAtom = atomWithInfiniteQuery(() => ({
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.response.count + 1,
queryKey: ['error test', 'count1Infinite'],
retry: false,
queryFn: async (): Promise<{ response: { count: number } }> => {
await new Promise<void>((r) => (resolve = r))
throw new Error('fetch error')
},
throwOnError: true,
}))
const Counter = () => {
const [{ data, isPending }] = useAtom(countAtom)

if (isPending) return <>loading</>

const pages = data?.pages

return (
<>
<div>count: {pages?.[0]?.response.count}</div>
</>
)
}

const { findByText } = render(
<StrictMode>
<ErrorBoundary>
<Suspense fallback="loading">
<Counter />
</Suspense>
</ErrorBoundary>
</StrictMode>
)

await findByText('loading')
resolve()
await findByText('errored')
})
})
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"postcompile": "cp dist/index.modern.mjs dist/index.modern.js && cp dist/index.modern.mjs.map dist/index.modern.js.map",
"test": "run-s eslint tsc-test jest",
"eslint": "eslint --ext .js,.ts,.tsx .",
"jest": "jest __tests__/atomWithMutation_spec.tsx --watch",
"jest": "jest __tests__/atomWithInfiniteQuery_spec.tsx --watch",
"tsc-test": "tsc --project . --noEmit",
"examples:01_typescript": "DIR=01_typescript EXT=tsx webpack serve",
"examples:02_refetch": "DIR=02_refetch EXT=tsx webpack serve",
Expand All @@ -53,7 +53,7 @@
],
"license": "MIT",
"devDependencies": {
"@tanstack/query-core": "^5.4.3",
"@tanstack/query-core": "^5.12.1",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.4",
"@types/node": "^20.5.9",
Expand Down
Loading

0 comments on commit 2207e4c

Please sign in to comment.