From 8d3fbde2a7a55b4b6c6703f5c5ce92bdf913c2d3 Mon Sep 17 00:00:00 2001 From: kalijonn <43421621+kalijonn@users.noreply.github.com> Date: Mon, 16 Oct 2023 04:50:32 +0530 Subject: [PATCH] arbitrary change --- __tests__/atomWithQuery_spec.tsx | 2 - __tests__/atomWithSuspenseQuery_spec.tsx | 198 +++++++++++++++++++++++ package.json | 2 +- src/atomWithSuspenseQuery.ts | 136 ++++++++++++++-- src/index.ts | 1 + src/utils.ts | 62 +++++++ tsconfig.json | 4 +- 7 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 __tests__/atomWithSuspenseQuery_spec.tsx create mode 100644 src/utils.ts diff --git a/__tests__/atomWithQuery_spec.tsx b/__tests__/atomWithQuery_spec.tsx index 82f9c71..221905d 100644 --- a/__tests__/atomWithQuery_spec.tsx +++ b/__tests__/atomWithQuery_spec.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { Component, ReactNode, @@ -16,7 +15,6 @@ beforeEach(() => { afterEach(() => { jest.runAllTimers() jest.useRealTimers() - jest.clearAllMocks() }) it('query basic test', async () => { diff --git a/__tests__/atomWithSuspenseQuery_spec.tsx b/__tests__/atomWithSuspenseQuery_spec.tsx new file mode 100644 index 0000000..11c8c43 --- /dev/null +++ b/__tests__/atomWithSuspenseQuery_spec.tsx @@ -0,0 +1,198 @@ +import React, { + Component, + ReactNode, + StrictMode, + Suspense, + useState, +} from 'react' +import { QueryClient } from '@tanstack/query-core' +import { fireEvent, render } from '@testing-library/react' +import { Getter, atom, useAtom, useSetAtom } from 'jotai' +import { + QueryErrorResetBoundary, + atomWithQuery, + atomWithSuspenseQuery, +} from '../src' +beforeEach(() => { + jest.useFakeTimers() +}) +afterEach(() => { + jest.runAllTimers() + jest.useRealTimers() +}) + +// it('suspense basic, suspends', async () => { +// let resolve = () => {} +// const countAtom = atomWithSuspenseQuery(() => ({ +// queryKey: ['test1'], +// queryFn: async () => { +// await new Promise((r) => (resolve = r)) +// return { response: { count: 0 } } +// }, +// })) +// const Counter = () => { +// const [countData] = useAtom(countAtom) +// console.log(JSON.stringify({ countData }, null, 2)) +// const { data } = countData +// return ( +// <> +//
count: {data?.response.count}
+// +// ) +// } + +// const { findByText } = render( +// +// +// +// +// +// ) + +// await findByText('loading') +// resolve() +// await findByText('count: 0') +// }) + +// it('query refetch', async () => { +// const mockFetch = jest.fn< +// { response: { message: string } }, +// { message: string }[] +// >((response) => ({ +// response, +// })) +// let resolve = () => {} +// const greetingAtom = atomWithSuspenseQuery(() => ({ +// queryKey: ['test3'], +// queryFn: async () => { +// await new Promise((r) => (resolve = r)) +// const response = mockFetch({ message: 'helloWorld' }) +// return response +// }, +// })) +// const Greeting = () => { +// const [{ data, refetch }] = useAtom(greetingAtom) + +// return ( +// <> +//
message: {data?.response.message}
+// +// +// ) +// } + +// const { findByText, getByText } = render( +// +// +// +// +// +// ) + +// await findByText('loading') +// resolve() +// await findByText('message: helloWorld') +// expect(mockFetch).toBeCalledTimes(1) + +// fireEvent.click(getByText('refetch')) +// await expect(() => findByText('loading')).rejects.toThrow() //refetch implementation in tanstack doesn't trigger loading state +// resolve() +// await findByText('message: helloWorld') +// expect(mockFetch).toBeCalledTimes(2) //this ensures we are actually running the query function again +// }) + +describe('intialData test', () => { + // it('query with initialData test', async () => { + // const mockFetch = jest.fn((response) => ({ response })) + // let resolve = () => {} + + // const countAtom = atomWithSuspenseQuery(() => ({ + // queryKey: ['initialData_count1'], + // queryFn: async () => { + // await new Promise((r) => (resolve = r)) + // return mockFetch({ count: 10 }) + // }, + // initialData: { response: { count: 0 } }, + // staleTime: 0, + // })) + // const Counter = () => { + // const [countData] = useAtom(countAtom) + // const { data, isError } = countData + + // if (isError) { + // return <>errorred + // } + + // const count = data?.response.count + // return ( + // <> + //
count: {count}
+ // + // ) + // } + + // const { findByText } = render( + // + // + // + // + // + // ) + + // // NOTE: the atom is never loading + // await expect(() => findByText('loading')).rejects.toThrow() + // await findByText('count: 0') + // resolve() + // await findByText('count: 10') + // expect(mockFetch).toHaveBeenCalledTimes(1) + // }) + + it('query with initialData test with dependency', async () => { + const mockFetch = jest.fn((response) => ({ response })) + let resolve = () => {} + const numberAtom = atom(10) + const countAtom = atomWithSuspenseQuery((get) => ({ + queryKey: ['initialData_count1', get(numberAtom)], + queryFn: async ({ queryKey: [, myNumber] }) => { + await new Promise((r) => (resolve = r)) + return mockFetch({ count: myNumber }) + }, + initialData: { response: { count: 0 } }, + staleTime: 0, + })) + const Counter = () => { + const [countData] = useAtom(countAtom) + const { data, isError } = countData + if (isError) { + return <>errorred + } + const count = data?.response.count + return ( + <> +
count: {count}
+ + ) + } + + const Increment = () => { + const setNumber = useSetAtom(numberAtom) + return + } + const { findByText } = render( + + + + + + + ) + // NOTE: the atom is never loading + await expect(() => findByText('loading')).rejects.toThrow() + await findByText('count: 0') + resolve() + await findByText('count: 10') + expect(mockFetch).toHaveBeenCalledTimes(1) + fireEvent.click(await findByText('increment')) + await findByText('count: 0') + }) +}) diff --git a/package.json b/package.json index ff4052e..d724a4a 100644 --- a/package.json +++ b/package.json @@ -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", + "jest": "jest __tests__/atomWithSuspenseQuery_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", diff --git a/src/atomWithSuspenseQuery.ts b/src/atomWithSuspenseQuery.ts index 4eab5bf..9b0c347 100644 --- a/src/atomWithSuspenseQuery.ts +++ b/src/atomWithSuspenseQuery.ts @@ -3,9 +3,14 @@ import { type QueryKey, QueryObserver, type QueryObserverOptions, + type QueryObserverResult, + type QueryObserverSuccessResult, } from '@tanstack/query-core' -import { Getter, atom } from 'jotai' +import { Atom, Getter, atom } from 'jotai' +import { filter, fromPromise, make, pipe, toObservable, toPromise } from 'wonka' +import { isResetAtom } from './QueryAtomErrorResetBoundary' import { queryClientAtom } from './queryClientAtom' +import { shouldSuspend } from './utils' export const atomWithSuspenseQuery = < TQueryFnData = unknown, @@ -13,7 +18,7 @@ export const atomWithSuspenseQuery = < TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, - TSuspense extends boolean = false, + TInitialData = TData, >( getOptions: ( get: Getter @@ -23,18 +28,125 @@ export const atomWithSuspenseQuery = < TData, TQueryData, TQueryKey - > & { suspense?: TSuspense }, + > & { initialData?: TInitialData }, getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom) ) => { - return atom((get) => { - const options = { ...getOptions(get), enabled: true, suspense: true } - const queryClient = getQueryClient(get) - const defaultedQueryOptions = queryClient.defaultQueryOptions(options) - const observer = new QueryObserver(queryClient, defaultedQueryOptions) - // const errorResetBoundary = get(errorResetBoundaryAtom) - - return observer.fetchOptimistic(defaultedQueryOptions).catch(() => { - //reset error boundary state + const IN_RENDER = Symbol() + + const queryClientAtom = atom(getQueryClient) + const optionsAtom = atom((get) => { + const client = get(queryClientAtom) + const options = getOptions(get) + return client.defaultQueryOptions({ + ...options, + enabled: true, + suspense: true, + }) + }) + + const observerCacheAtom = atom( + () => + new WeakMap< + QueryClient, + QueryObserver + >() + ) + + const observerAtom = atom((get) => { + const isReset = get(isResetAtom) + + const options = get(optionsAtom) + const client = get(queryClientAtom) + const observerCache = get(observerCacheAtom) + + const observer = observerCache.get(client) + + if (isReset) { + if (observer) { + observerCache.delete(client) + observer.remove() + } + const newObserver = new QueryObserver(client, options) + observerCache.set(client, newObserver) + return newObserver + } + + if (observer) { + ;(observer as any)[IN_RENDER] = true + observer.setOptions(options) + delete (observer as any)[IN_RENDER] + + return observer + } + + const newObserver = new QueryObserver(client, options) + observerCache.set(client, newObserver) + + return newObserver + }) + + const observableAtom = atom((get) => { + const observer = get(observerAtom) + const source = make>(({ next }) => { + const callback = (result: QueryObserverResult) => { + const notifyResult = () => next(result) + + if ((observer as any)[IN_RENDER]) { + Promise.resolve().then(notifyResult) + } else { + notifyResult() + } + } + + const unsubscribe = observer.subscribe(callback) + return () => unsubscribe() }) + return pipe( + source, + filter((state) => !state.isFetching), + toObservable + ) + }) + + const dataAtom = atom((get) => { + const observer = get(observerAtom) + const observable = get(observableAtom) + + const currentResult = observer.getCurrentResult() + const resultAtom = atom(currentResult) + + resultAtom.onMount = (set) => { + const { unsubscribe } = observable.subscribe((state) => { + set(state) + }) + return () => unsubscribe() + } + + return resultAtom + }) + + return atom((get) => { + const options = get(optionsAtom) + const observer = get(observerAtom) + const optimisticResult = observer.getOptimisticResult(options) + console.log({ defaultedOptions: options }) + const suspend = shouldSuspend(options, optimisticResult, false) + console.log({ suspend }) + + if (suspend) { + return observer.fetchOptimistic(options) + } + + const shouldThrowError = + optimisticResult.isError && !optimisticResult.isFetching + + if (shouldThrowError) { + throw optimisticResult.error + } + + const resultAtom = get(dataAtom) + const result = get(resultAtom) + + return result }) } diff --git a/src/index.ts b/src/index.ts index 9d50663..88fc703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,4 +4,5 @@ export { atomsWithInfiniteQuery } from './atomsWithInfiniteQuery' export { atomsWithMutation } from './atomsWithMutation' export { atomsWithQueryAsync } from './atomsWithQueryAsync' export { atomWithQuery } from './atomWithQuery' +export { atomWithSuspenseQuery } from './atomWithSuspenseQuery' export * from './QueryAtomErrorResetBoundary' diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0d9cca5 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,62 @@ +import type { + DefaultedQueryObserverOptions, + Query, + QueryKey, + QueryObserverResult, + UseErrorBoundary, +} from '@tanstack/query-core' +import { QueryErrorResetBoundaryValue } from './QueryAtomErrorResetBoundary' + +export const shouldSuspend = ( + defaultedOptions: + | DefaultedQueryObserverOptions + | undefined, + result: QueryObserverResult, + isRestoring: boolean +) => defaultedOptions?.suspense && willFetch(result, isRestoring) + +export const willFetch = ( + result: QueryObserverResult, + isRestoring: boolean +) => result.isLoading && !isRestoring + +export const getHasError = < + TData, + TError, + TQueryFnData, + TQueryData, + TQueryKey extends QueryKey, +>({ + result, + errorResetBoundary, + useErrorBoundary, + query, +}: { + result: QueryObserverResult + errorResetBoundary: QueryErrorResetBoundaryValue + useErrorBoundary: UseErrorBoundary< + TQueryFnData, + TError, + TQueryData, + TQueryKey + > + query: Query +}) => { + return ( + result.isError && + !errorResetBoundary.isReset() && + !result.isFetching && + shouldThrowError(useErrorBoundary, [result.error, query]) + ) +} +export function shouldThrowError boolean>( + _useErrorBoundary: boolean | T | undefined, + params: Parameters +): boolean { + // Allow useErrorBoundary function to override throwing behavior on a per-error basis + if (typeof _useErrorBoundary === 'function') { + return _useErrorBoundary(...params) + } + + return !!_useErrorBoundary +} diff --git a/tsconfig.json b/tsconfig.json index 3dd8b60..41d007b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,8 @@ "module": "es2015", "moduleResolution": "node", "allowJs": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "jsx": "react",