diff --git a/__tests__/01_basic_spec.tsx b/__tests__/01_basic_spec.tsx index 3cd0034..01d452f 100644 --- a/__tests__/01_basic_spec.tsx +++ b/__tests__/01_basic_spec.tsx @@ -1,7 +1,10 @@ import { atomWithInfiniteQuery, atomWithMutation, + atomWithMutationState, atomWithQuery, + atomWithSuspenseInfiniteQuery, + atomWithSuspenseQuery, queryClientAtom, } from '../src/index' @@ -11,5 +14,8 @@ describe('basic spec', () => { expect(atomWithQuery).toBeDefined() expect(atomWithInfiniteQuery).toBeDefined() expect(atomWithMutation).toBeDefined() + expect(atomWithSuspenseQuery).toBeDefined() + expect(atomWithSuspenseInfiniteQuery).toBeDefined() + expect(atomWithMutationState).toBeDefined() }) }) diff --git a/__tests__/atomWithInfiniteQuery_spec.tsx b/__tests__/atomWithInfiniteQuery_spec.tsx index cdea95b..f562a3a 100644 --- a/__tests__/atomWithInfiniteQuery_spec.tsx +++ b/__tests__/atomWithInfiniteQuery_spec.tsx @@ -5,14 +5,6 @@ import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' 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 } } @@ -188,8 +180,6 @@ it('infinite query with enabled', async () => { }) it('infinite query with enabled 2', async () => { - jest.useRealTimers() // FIXME can avoid? - const enabledAtom = atom(true) const slugAtom = atom('first') type DataResponse = { @@ -198,6 +188,7 @@ it('infinite query with enabled 2', async () => { currentPage: number } } + let resolve = () => {} const slugQueryAtom = atomWithInfiniteQuery((get) => { const slug = get(slugAtom) const isEnabled = get(enabledAtom) @@ -207,7 +198,7 @@ it('infinite query with enabled 2', async () => { enabled: isEnabled, queryKey: ['enabled_toggle'], queryFn: async ({ pageParam }) => { - await new Promise((r) => setTimeout(r, 100)) // FIXME can avoid? + await new Promise((r) => (resolve = r)) return { response: { slug: `hello-${slug}`, currentPage: pageParam as number }, } @@ -264,17 +255,15 @@ it('infinite query with enabled 2', async () => { ) await findByText('loading') + resolve() 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')) + resolve() await findByText('slug: hello-world') }) diff --git a/__tests__/atomWithMutationState_spec.tsx b/__tests__/atomWithMutationState_spec.tsx new file mode 100644 index 0000000..5dc2f23 --- /dev/null +++ b/__tests__/atomWithMutationState_spec.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { QueryClient } from '@tanstack/query-core' +import { fireEvent, render } from '@testing-library/react' +import { Provider, useAtom } from 'jotai' +import { atomWithMutation, atomWithMutationState } from '../src' + +it('atomWithMutationState multiple', async () => { + const client = new QueryClient() + let resolve1: (() => void) | undefined + const mutateAtom1 = atomWithMutation( + () => ({ + mutationKey: ['test-atom'], + mutationFn: async (a) => { + await new Promise((r) => { + resolve1 = r + }) + return a + }, + }), + () => client + ) + let resolve2: (() => void) | undefined + const mutateAtom2 = atomWithMutation( + () => ({ + mutationKey: ['test-atom'], + mutationFn: async (a) => { + await new Promise((r) => { + resolve2 = r + }) + return a + }, + }), + () => client + ) + + const mutationStateAtom = atomWithMutationState( + () => ({ filters: { mutationKey: ['test-atom'] } }), + () => client + ) + + function App() { + const [{ mutate: mutate1 }] = useAtom(mutateAtom1) + const [{ mutate: mutate2 }] = useAtom(mutateAtom2) + const [mutations] = useAtom(mutationStateAtom) + + return ( +
+

mutationCount: {mutations.length}

+ +
+ ) + } + + const { findByText, getByText } = render( + + + + ) + + await findByText('mutationCount: 0') + fireEvent.click(getByText('mutate')) + await findByText('mutationCount: 2') + resolve1?.() + await findByText('mutationCount: 1') + resolve2?.() + await findByText('mutationCount: 0') +}) diff --git a/__tests__/atomWithSuspenseInfiniteQuery_spec.tsx b/__tests__/atomWithSuspenseInfiniteQuery_spec.tsx index d195923..81e0625 100644 --- a/__tests__/atomWithSuspenseInfiniteQuery_spec.tsx +++ b/__tests__/atomWithSuspenseInfiniteQuery_spec.tsx @@ -2,13 +2,6 @@ import React, { StrictMode, Suspense } from 'react' import { fireEvent, render } from '@testing-library/react' import { useAtom } from 'jotai' import { atomWithSuspenseInfiniteQuery } from '../src' -beforeEach(() => { - jest.useFakeTimers() -}) -afterEach(() => { - jest.runAllTimers() - jest.useRealTimers() -}) it('suspense basic, suspends', async () => { let resolve = () => {} diff --git a/__tests__/atomWithSuspenseQuery_spec.tsx b/__tests__/atomWithSuspenseQuery_spec.tsx index be2883f..8929b36 100644 --- a/__tests__/atomWithSuspenseQuery_spec.tsx +++ b/__tests__/atomWithSuspenseQuery_spec.tsx @@ -3,13 +3,6 @@ import { QueryClient } from '@tanstack/query-core' import { fireEvent, render } from '@testing-library/react' import { atom, useAtom, useSetAtom } from 'jotai' import { atomWithSuspenseQuery } from '../src' -beforeEach(() => { - jest.useFakeTimers() -}) -afterEach(() => { - jest.runAllTimers() - jest.useRealTimers() -}) it('suspense basic, suspends', async () => { let resolve = () => {} diff --git a/package.json b/package.json index 6819fed..dbc5068 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 --watch", + "jest": "jest", "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/atomWithMutationState.ts b/src/atomWithMutationState.ts new file mode 100644 index 0000000..07d2069 --- /dev/null +++ b/src/atomWithMutationState.ts @@ -0,0 +1,55 @@ +import { + DefaultError, + Mutation, + MutationCache, + MutationFilters, + MutationState, + QueryClient, +} from '@tanstack/query-core' +import { Getter, atom } from 'jotai' +import { queryClientAtom } from './queryClientAtom' + +type MutationStateOptions = { + filters?: MutationFilters + select?: ( + mutation: Mutation + ) => TResult +} + +function getResult( + mutationCache: MutationCache, + options: MutationStateOptions +): Array { + return mutationCache + .findAll({ ...options.filters, status: 'pending' }) + .map( + (mutation): TResult => + (options.select + ? options.select( + mutation as Mutation + ) + : mutation.state) as TResult + ) +} + +export const atomWithMutationState = ( + getOptions: (get: Getter) => MutationStateOptions, + getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom) +) => { + const resultsAtom = atom([]) + const observableAtom = atom((get) => { + const queryClient = getQueryClient(get) + + const mutationCache = queryClient.getMutationCache() + resultsAtom.onMount = (set) => { + mutationCache.subscribe(() => { + set(getResult(getQueryClient(get).getMutationCache(), getOptions(get))) + }) + } + }) + + return atom((get) => { + get(observableAtom) + return get(resultsAtom) + }) +} diff --git a/src/index.ts b/src/index.ts index 988b638..45050ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,6 @@ export { atomWithSuspenseQuery } from './atomWithSuspenseQuery' export { atomWithInfiniteQuery } from './atomWithInfiniteQuery' export { atomWithMutation } from './atomWithMutation' export { atomWithSuspenseInfiniteQuery } from './atomWithSuspenseInfiniteQuery' +export { atomWithMutationState } from './atomWithMutationState' export * from './QueryAtomErrorResetBoundary'