Skip to content

fix(angular-query): ensure initial mutation pending state is emitted #9098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ describe('injectMutationState', () => {
<span>{{ mutation.status }}</span>
}
`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ describe('injectMutation', () => {
}))
})

TestBed.tick()

mutation.mutate(result)
vi.advanceTimersByTime(1)

Expand Down Expand Up @@ -133,9 +131,17 @@ describe('injectMutation', () => {

mutation.mutate('xyz')

const mutations = mutationCache.find({ mutationKey: ['2'] })
mutationKey.set(['3'])

mutation.mutate('xyz')

expect(mutations?.options.mutationKey).toEqual(['2'])
expect(mutationCache.find({ mutationKey: ['1'] })).toBeUndefined()
expect(
mutationCache.find({ mutationKey: ['2'] })?.options.mutationKey,
).toEqual(['2'])
expect(
mutationCache.find({ mutationKey: ['3'] })?.options.mutationKey,
).toEqual(['3'])
})

test('should reset state after invoking mutation.reset', async () => {
Expand Down Expand Up @@ -310,7 +316,6 @@ describe('injectMutation', () => {
<button (click)="mutate()"></button>
<span>{{ mutation.data() }}</span>
`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
Expand All @@ -333,8 +338,6 @@ describe('injectMutation', () => {
button.triggerEventHandler('click')

await resolveMutations()
fixture.detectChanges()

const text = debugElement.query(By.css('span')).nativeElement.textContent
expect(text).toEqual('value')
const mutation = mutationCache.find({ mutationKey: ['fake', 'value'] })
Expand All @@ -351,7 +354,6 @@ describe('injectMutation', () => {
<button (click)="mutate()"></button>
<span>{{ mutation.data() }}</span>
`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
Expand Down Expand Up @@ -394,6 +396,37 @@ describe('injectMutation', () => {
expect(mutation2!.options.mutationKey).toEqual(['fake', 'updatedValue'])
})

test('should have pending state when mutating in constructor', async () => {
@Component({
selector: 'app-fake',
template: `
<span>{{ mutation.isPending() ? 'pending' : 'not pending' }}</span>
`,
})
class FakeComponent {
mutation = injectMutation(() => ({
mutationKey: ['fake'],
mutationFn: () => sleep(0).then(() => 'fake'),
}))

constructor() {
this.mutation.mutate()
}
}

const fixture = TestBed.createComponent(FakeComponent)
const { debugElement } = fixture
const span = debugElement.query(By.css('span'))

vi.advanceTimersByTime(1)

expect(span.nativeElement.textContent).toEqual('pending')

await resolveMutations()

expect(span.nativeElement.textContent).toEqual('not pending')
})

describe('throwOnError', () => {
test('should evaluate throwOnError when mutation is expected to throw', async () => {
const err = new Error('Expected mock error. All is well!')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,6 @@ describe('injectQuery', () => {
@Component({
selector: 'app-fake',
template: `{{ query.data() }}`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
Expand Down
89 changes: 37 additions & 52 deletions packages/angular-query-experimental/src/inject-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
NgZone,
assertInInjectionContext,
computed,
effect,
inject,
signal,
untracked,
Expand Down Expand Up @@ -71,7 +70,15 @@ export function injectMutation<
null

return computed(() => {
return (instance ||= new MutationObserver(queryClient, optionsSignal()))
const observerOptions = optionsSignal()
return untracked(() => {
if (instance) {
instance.setOptions(observerOptions)
} else {
instance = new MutationObserver(queryClient, observerOptions)
}
return instance
})
})
})()

Expand All @@ -84,14 +91,6 @@ export function injectMutation<
}
})

/**
* Computed signal that gets result from mutation cache based on passed options
*/
const resultFromInitialOptionsSignal = computed(() => {
const observer = observerSignal()
return observer.getCurrentResult()
})

/**
* Signal that contains result set by subscriber
*/
Expand All @@ -102,50 +101,36 @@ export function injectMutation<
TContext
> | null>(null)

effect(
() => {
const observer = observerSignal()
const observerOptions = optionsSignal()

untracked(() => {
observer.setOptions(observerOptions)
})
},
{
injector,
},
)

effect(
() => {
// observer.trackResult is not used as this optimization is not needed for Angular
const observer = observerSignal()
/**
* Computed signal that gets result from mutation cache based on passed options
*/
const resultFromInitialOptionsSignal = computed(() => {
const observer = observerSignal()

untracked(() => {
const unsubscribe = ngZone.runOutsideAngular(() =>
observer.subscribe(
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [state.error])
) {
ngZone.onError.emit(state.error)
throw state.error
}
untracked(() => {
const unsubscribe = ngZone.runOutsideAngular(() =>
// observer.trackResult is not used as this optimization is not needed for Angular
observer.subscribe(
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [state.error])
) {
ngZone.onError.emit(state.error)
throw state.error
}

resultFromSubscriberSignal.set(state)
})
}),
),
)
destroyRef.onDestroy(unsubscribe)
})

resultFromSubscriberSignal.set(state)
})
}),
),
)
destroyRef.onDestroy(unsubscribe)
})
},
{
injector,
},
)
return observer.getCurrentResult()
})

const resultSignal = computed(() => {
const resultFromSubscriber = resultFromSubscriberSignal()
Expand Down