Skip to main content
Module

x/jotai/tests/query/atomWithQuery.test.tsx

👻 Primitive and flexible state management for React
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
import { Component, Suspense, useContext, useState } from 'react'import type { ReactNode } from 'react'import { fireEvent, render } from '@testing-library/react'import { atom, SECRET_INTERNAL_getScopeContext as getScopeContext, useAtom, useSetAtom,} from 'jotai'import { atomWithQuery } from 'jotai/query'import { getTestProvider } from '../testUtils'import fakeFetch from './fakeFetch'
const Provider = getTestProvider()
// This is only used to pass tests with unstable_enableVersionedWriteconst useRetryFromError = (scope?: symbol | string | number) => { const ScopeContext = getScopeContext(scope) const { r: retryFromError } = useContext(ScopeContext) return retryFromError || ((fn) => fn())}
it('query basic test', async () => { const countAtom = atomWithQuery(() => ({ queryKey: ['count1'], queryFn: async () => { return await fakeFetch({ count: 0 }, false, 100) }, })) const Counter = () => { const [ { response: { count }, }, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> </> ) }
const { findByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> </Provider> )
await findByText('loading') await findByText('count: 0')})
it('query basic test with object instead of function', async () => { const countAtom = atomWithQuery({ queryKey: ['count2'], queryFn: async () => { return await fakeFetch({ count: 0 }, false, 100) }, }) const Counter = () => { const [ { response: { count }, }, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> </> ) }
const { findByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> </Provider> )
await findByText('loading') await findByText('count: 0')})
it('query refetch', async () => { let count = 0 const mockFetch = jest.fn(fakeFetch) const countAtom = atomWithQuery(() => ({ queryKey: ['count3'], queryFn: async () => { const response = await mockFetch({ count }, false, 100) ++count return response }, })) const Counter = () => { const [ { response: { count }, }, dispatch, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> <button onClick={() => dispatch({ type: 'refetch' })}>refetch</button> </> ) }
const { findByText, getByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> </Provider> )
await findByText('loading') await findByText('count: 0') expect(mockFetch).toBeCalledTimes(1)
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch'))
await findByText('loading') await findByText('count: 1') expect(mockFetch).toBeCalledTimes(2)})
it('query loading', async () => { let count = 0 const mockFetch = jest.fn(fakeFetch) const countAtom = atomWithQuery(() => ({ queryKey: ['count4'], queryFn: async () => { const response = await mockFetch({ count }, false, 100) ++count return response }, })) const derivedAtom = atom((get) => get(countAtom)) const dispatchAtom = atom(null, (_get, set, action: any) => { set(countAtom, action) }) const Counter = () => { const [ { response: { count }, }, ] = useAtom(derivedAtom) return ( <> <div>count: {count}</div> </> ) } const RefreshButton = () => { const [, dispatch] = useAtom(dispatchAtom) return ( <button onClick={() => dispatch({ type: 'refetch' })}>refetch</button> ) }
const { findByText, getByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> <RefreshButton /> </Provider> )
await findByText('loading') await findByText('count: 0')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await findByText('loading') await findByText('count: 1')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await findByText('loading') await findByText('count: 2')})
it('query loading 2', async () => { let count = 0 const mockFetch = jest.fn(fakeFetch) const countAtom = atomWithQuery(() => ({ queryKey: ['count5'], queryFn: async () => { const response = await mockFetch({ count }, false, 100) ++count return response }, }))
const Counter = () => { const [ { response: { count }, }, dispatch, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> <button onClick={() => dispatch({ type: 'refetch' })}>refetch</button> </> ) } const { findByText, getByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> </Provider> )
await findByText('loading') await findByText('count: 0')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await findByText('loading') await findByText('count: 1')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await findByText('loading') await findByText('count: 2')})
it('query no-loading with keepPreviousData', async () => { const dataAtom = atom(0) const mockFetch = jest.fn(fakeFetch) const countAtom = atomWithQuery((get) => ({ queryKey: ['keepPreviousData', get(dataAtom)], keepPreviousData: true, queryFn: async () => { const response = await mockFetch({ count: get(dataAtom) }, false, 100) return response }, })) const Counter = () => { const [ { response: { count }, }, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> </> ) } const RefreshButton = () => { const [data, setData] = useAtom(dataAtom) return <button onClick={() => setData(data + 1)}>refetch</button> }
const { findByText, getByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> <RefreshButton /> </Provider> )
await findByText('loading') await findByText('count: 0')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await expect(() => findByText('loading')).rejects.toThrow() await findByText('count: 1')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await expect(() => findByText('loading')).rejects.toThrow() await findByText('count: 2')})
it('query with enabled', async () => { const slugAtom = atom<string | null>(null) const mockFetch = jest.fn(fakeFetch) const slugQueryAtom = atomWithQuery((get) => { const slug = get(slugAtom) return { enabled: !!slug, queryKey: ['disabled_until_value', slug], queryFn: async () => { return await mockFetch({ slug: `hello-${slug}` }, false, 100) }, } })
const Slug = () => { const [data] = useAtom(slugQueryAtom) if (!data?.response?.slug) return <div>not enabled</div> return <div>slug: {data?.response?.slug}</div> }
const Parent = () => { const [, setSlug] = useAtom(slugAtom) return ( <div> <button onClick={() => { setSlug('world') }}> set slug </button> <Slug /> </div> ) }
const { getByText, findByText } = render( <Provider> <Suspense fallback="loading"> <Parent /> </Suspense> </Provider> )
await findByText('not enabled') expect(mockFetch).toHaveBeenCalledTimes(0)
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('set slug')) await findByText('loading') await findByText('slug: hello-world') expect(mockFetch).toHaveBeenCalledTimes(1)})
it('query with enabled 2', async () => { const mockFetch = jest.fn(fakeFetch) const enabledAtom = atom<boolean>(true) const slugAtom = atom<string | null>('first')
const slugQueryAtom = atomWithQuery((get) => { const slug = get(slugAtom) const isEnabled = get(enabledAtom) return { enabled: isEnabled, queryKey: ['enabled_toggle'], queryFn: async () => { return await mockFetch({ slug: `hello-${slug}` }, false, 100) }, } })
const Slug = () => { const [data] = useAtom(slugQueryAtom) if (!data?.response?.slug) return <div>not enabled</div> return <div>slug: {data?.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( <Provider> <Suspense fallback="loading"> <Parent /> </Suspense> </Provider> ) await findByText('loading') expect(mockFetch).toHaveBeenCalledTimes(1) await findByText('slug: hello-first')
fireEvent.click(getByText('set disabled')) fireEvent.click(getByText('set slug'))
await new Promise((r) => setTimeout(r, 100)) await findByText('slug: hello-first') expect(mockFetch).toHaveBeenCalledTimes(1)
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('set enabled')) await findByText('slug: hello-world') expect(mockFetch).toHaveBeenCalledTimes(2)})
it('query with enabled (#500)', async () => { const enabledAtom = atom(true) const countAtom = atomWithQuery((get) => { const enabled = get(enabledAtom) return { enabled, queryKey: ['count_500_issue'], queryFn: async () => { return await fakeFetch({ count: 1 }, false, 100) }, } })
const Counter = () => { const [value] = useAtom(countAtom) if (!value) return null const { response: { count }, } = value return <div>count: {count}</div> }
const Parent = () => { const [showChildren, setShowChildren] = useState(true) const [, setEnabled] = useAtom(enabledAtom) return ( <div> <button onClick={() => { setShowChildren((x) => !x) setEnabled((x) => !x) }}> toggle </button> {showChildren ? <Counter /> : <div>hidden</div>} </div> ) }
const { getByText, findByText } = render( <Provider> <Suspense fallback="loading"> <Parent /> </Suspense> </Provider> )
await findByText('loading') await findByText('count: 1')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('toggle')) await findByText('hidden')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('toggle')) await findByText('count: 1')})
it('query with initialData test', async () => { const mockFetch = jest.fn(fakeFetch)
const countAtom = atomWithQuery(() => ({ queryKey: ['initialData_count1'], queryFn: async () => { return await mockFetch({ count: 10 }) }, initialData: { response: { count: 0 } }, refetchInterval: 100, })) const Counter = () => { const [ { response: { count }, }, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> </> ) }
const { findByText } = render( <Provider> <Counter /> </Provider> )
// NOTE: the atom never suspends await findByText('count: 0') await findByText('count: 10') expect(mockFetch).toHaveBeenCalledTimes(1)})
it('query dependency test', async () => { const baseCountAtom = atom(0) const incrementAtom = atom(null, (_get, set) => set(baseCountAtom, (c) => c + 1) ) const countAtom = atomWithQuery((get) => ({ queryKey: ['count_with_dependency', get(baseCountAtom)], queryFn: async () => { return await fakeFetch({ count: get(baseCountAtom) }, false, 100) }, }))
const Counter = () => { const [ { response: { count }, }, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> </> ) }
const Controls = () => { const [, increment] = useAtom(incrementAtom) return <button onClick={increment}>increment</button> }
const { getByText, findByText } = render( <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> <Controls /> </Provider> )
await findByText('loading') await findByText('count: 0')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('increment')) await findByText('loading') await findByText('count: 1')})
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 () => { const countAtom = atomWithQuery(() => ({ queryKey: ['error test', 'count1'], retry: false, queryFn: async () => { return await fakeFetch({ count: 0 }, true, 100) }, })) const Counter = () => { const [ { response: { count }, }, ] = useAtom(countAtom) return ( <> <div>count: {count}</div> </> ) }
const { findByText } = render( <Provider> <ErrorBoundary> <Suspense fallback="loading"> <Counter /> </Suspense> </ErrorBoundary> </Provider> )
await findByText('loading') await findByText('errored') })
it('can recover from error', async () => { let count = 0 let willThrowError = true const countAtom = atomWithQuery(() => ({ queryKey: ['error test', 'count2'], retry: false, staleTime: 200, queryFn: () => { const promise = fakeFetch({ count }, willThrowError, 200) willThrowError = !willThrowError ++count return promise }, })) const Counter = () => { const [ { response: { count }, }, dispatch, ] = useAtom(countAtom) const refetch = () => dispatch({ type: 'refetch' }) return ( <> <div>count: {count}</div> <button onClick={refetch}>refetch</button> </> ) }
const App = () => { const dispatch = useSetAtom(countAtom) const retryFromError = useRetryFromError() const retry = () => { retryFromError(() => { dispatch({ type: 'refetch' }) }) } return ( <ErrorBoundary retry={retry}> <Suspense fallback="loading"> <Counter /> </Suspense> </ErrorBoundary> ) }
const { findByText, getByText } = render( <Provider> <App /> </Provider> )
await findByText('loading') await findByText('errored')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('retry')) await findByText('loading') await findByText('count: 1')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('refetch')) await findByText('loading') await findByText('errored')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('retry')) await findByText('loading') await findByText('count: 3') })})