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
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
@@ -144,7 +144,6 @@ describe('injectMutationState', () => {
<span>{{ mutation.status }}</span>
}
`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
Original file line number Diff line number Diff line change
@@ -58,8 +58,6 @@ describe('injectMutation', () => {
}))
})

TestBed.tick()

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

@@ -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 () => {
@@ -310,7 +316,6 @@ describe('injectMutation', () => {
<button (click)="mutate()"></button>
<span>{{ mutation.data() }}</span>
`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
@@ -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'] })
@@ -351,7 +354,6 @@ describe('injectMutation', () => {
<button (click)="mutate()"></button>
<span>{{ mutation.data() }}</span>
`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
@@ -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!')
Original file line number Diff line number Diff line change
@@ -520,7 +520,6 @@ describe('injectQuery', () => {
@Component({
selector: 'app-fake',
template: `{{ query.data() }}`,
standalone: true,
})
class FakeComponent {
name = input.required<string>()
89 changes: 37 additions & 52 deletions packages/angular-query-experimental/src/inject-mutation.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ import {
NgZone,
assertInInjectionContext,
computed,
effect,
inject,
signal,
untracked,
@@ -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
})
})
})()

@@ -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
*/
@@ -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()