query icon indicating copy to clipboard operation
query copied to clipboard

ApplicationRef.isStable() is not emitting?

Open ejthan opened this issue 1 year ago • 13 comments

Which @ngneat/query-* package(s) are the source of the bug?

query

Is this a regression?

Yes

Description

If i use this library with provideClientHydration (ssr) i get the following message after 10 seconds:

NG0506: Angular hydration expected the ApplicationRef.isStable() to emit true, but it didn't happen within 10000ms. Angular hydration logic depends on the application becoming stable as a signal to complete hydration process.

There is a macrotask "setTimeout" which causes this. Here is the stack:

Error: Task 'macroTask' from 'setTimeout'. at TaskTrackingZoneSpec.onScheduleTask (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/zone__js_plugins_task-tracking.js?v=61f6983d:25:32) at _ZoneDelegate.scheduleTask (http://localhost:4200/polyfills.js:305:43) at Object.onScheduleTask (http://localhost:4200/polyfills.js:229:61) at _ZoneDelegate.scheduleTask (http://localhost:4200/polyfills.js:305:43) at _Zone.scheduleTask (http://localhost:4200/polyfills.js:172:35) at _Zone.scheduleMacroTask (http://localhost:4200/polyfills.js:190:19) at scheduleMacroTaskWithCurrentZone (http://localhost:4200/polyfills.js:541:23) at http://localhost:4200/polyfills.js:1991:20 at proto. (http://localhost:4200/polyfills.js:780:16) at Query.scheduleGc (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-HRNDKNRI.js?v=8c38df9a:540:25) at Object.onSuccess (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-HRNDKNRI.js?v=8c38df9a:793:16) at resolve (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-HRNDKNRI.js?v=8c38df9a:373:25) at _ZoneDelegate.invoke (http://localhost:4200/polyfills.js:294:158) at Object.onInvoke (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-PQEXWVRU.js?v=8c38df9a:9311:25) at _ZoneDelegate.invoke (http://localhost:4200/polyfills.js:294:46) at _Zone.run (http://localhost:4200/polyfills.js:100:35) at http://localhost:4200/polyfills.js:1028:28 at _ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:320:171) at http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-PQEXWVRU.js?v=8c38df9a:9129:49 at AsyncStackTaggingZoneSpec.onInvokeTask (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-PQEXWVRU.js?v=8c38df9a:9129:30) at _ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:320:54) at TaskTrackingZoneSpec.onInvokeTask (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/zone__js_plugins_task-tracking.js?v=61f6983d:50:31) at _ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:320:54) at Object.onInvokeTask (http://localhost:4200/@fs/xxx/.angular/cache/17.1.1/vite/deps/chunk-PQEXWVRU.js?v=8c38df9a:9300:25) at _ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:320:54) at _Zone.runTask (http://localhost:4200/polyfills.js:137:37) at drainMicroTaskQueue (http://localhost:4200/polyfills.js:475:23)

Looks like Query.scheduleGc is the problem.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

NG0506: Angular hydration expected the ApplicationRef.isStable() to emit `true`, but it didn't happen within 10000ms. Angular hydration logic depends on the application becoming stable as a signal to complete hydration process.

Please provide the environment you discovered this bug in

No response

Anything else?

No response

Do you want to create a pull request?

Yes

ejthan avatar Jun 14 '24 07:06 ejthan

You're welcome to create a PR

NetanelBasal avatar Jun 14 '24 14:06 NetanelBasal

@NetanelBasal Do you think that we could just exclude the whole lib/or only the server part from NgZone? For the experimental NoOp Zone in v18 it should imo work just fine. For <= v17 it should be excluded from the zone maybe by wrapping the provider?

It would probably be the second inject that should be excluded right? https://github.com/ngneat/query/blob/60684b2c499c2a07cf6057748b82a9bf4d0a261e/query/src/lib/query-client.ts#L52-L60

luii avatar Jun 14 '24 17:06 luii

Is your suggestion to do something like this?

return inject(NgZone).runOutsideAngular(() => inject(QueryClientToken))

ejthan avatar Jun 15 '24 07:06 ejthan

Yes, since NgZone is picking up on the timers from the Garbage Collector of Tanstack, this would probably work. I dont see any problem with this approach, tanstack has no relations to angular itself, the only thing that is being used here are the signals, which, fortunately enough, work without having the zone enabled. Same goes for the observables, these should also run without ngzone.

Baseline being: either exclude them from the zone or patch out only the the timers from it

luii avatar Jun 15 '24 07:06 luii

I have tested the whole thing with the following changes, but nothing has changed and I still get the same error message.

const QueryClientService = new InjectionToken<QueryClient>(
  'QueryClientService',
  {
    providedIn: 'root',
    factory() {
      if (isPlatformBrowser(inject(PLATFORM_ID))) {
        inject(QueryClientMount);
      }

      return inject(NgZone).runOutsideAngular(() => inject(QueryClientToken));
    }
  }
);

I tried that too:

const QueryClientToken = new InjectionToken<QueryClient>('QueryClient', {
  providedIn: 'root',
  factory() {
    return inject(NgZone).runOutsideAngular(() => new QueryClient(inject(QUERY_CLIENT_OPTIONS)));
  }
});

ejthan avatar Jun 15 '24 20:06 ejthan

I cant think of any other thing then, which could fix that, this is also Angular's recommendation on their new documentation: https://angular.dev/errors/NG0506#third-party-libraries

luii avatar Jun 17 '24 06:06 luii

i think i have similar issue, so i switched to experimental zone less and the problem from angular is gone, but there was another one with waiting for query results to finish fetching, so my very first not deeply tested solution is https://angular.dev/guide/experimental/zoneless#pendingtasks-for-server-side-rendering-ssr

 return this.#query({
      queryKey: [ChatQueryKeys.chatGroupsStats] as const,
      queryFn: () => {
        const taskCleanup = this.taskService.add();
        return this.chatRepository.fetchChatGroupsStats().pipe(
          delay(5000),
          tap((resp) => {
            setTimeout( () => taskCleanup(), 0)
          })
        );
      }
    }).result;

so it waits 5s on the server before marking the app as stable, so all needed data is there. Seems it is doing its job, but not really like it ...

radekdob avatar Jan 01 '25 00:01 radekdob

Could you briefly explain me why you're queue'ing a new macrotask (i.e. setTimeout( () => taskCleanup(), 0) after your response arrived? Iirc observeOn(asapScheduler) can also be used right before you execute the tap's side effect since its downstreaming. (But keep in mind that when you want to reuse the pipe somewhere else that you switch back the scheduler afterwards):

return this.chatRepository.fetchChatGroupsStats().pipe(
  delay(5000),
  observeOn(asapScheduler),
  tap((resp) => taskCleanup())

asapScheduler overseveOn

luii avatar Jan 01 '25 10:01 luii

@luii I suspect that taskCleanup() must be somehow deferred because without that it is(probably) cleaned up before template consume that signal value and server marks app as stable a bit to early(so data is fetched, but templated does not render value from it).

observeOn(asapScheduler) yeah, that also works. Thanks for hint, I see this operator for the first time and seems it is better than setTimeout because it should be executed earlier than macro task.

radekdob avatar Jan 01 '25 12:01 radekdob

Okay if i see and understand that all correctly, tanstack is doing everything right but it's not blocking the ApplicationRef from becoming stable as long as your values haven't yet arrived, even with PendingTasks involved, is that correct?

What i believe the problem currently is, is that you're missing some initialData and your query then returns the loading state, though im very inexpierienced with SSR (and haven't had much time to invest into it) i know that the HttpClient under the hood uses PendingTasks to block ApplicationRef from becoming stable (whilst a HEAD or GET is in action (POST can be added too)). You're trying to stop it from becoming stable and wait for the whole data, if you provide initial data (which could be an empty array, or somithing similiar) then you can render everthing, and get subsequent partial updates from the server. Atleast this is what i think you want to do, correct?

From what is written in the Tanstack Docs its appearent that they also use initialData to prepopulate their queries.

The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery.

luii avatar Jan 01 '25 13:01 luii

Yes, if the query is fast enough so sometimes it on time before making app as stable and gets rendered in initial html, but sometimes not or simply other async task take longer and block app from becoming stable :)

Angular SSR itself currently does not support concept of server side props, so what is left it is hydrate and transfer state. IMO passing initial data to every query might be a bit messy.

What i wanted to achieve is fetch chat unread messages on the server, render html with navbar including unread counter and use new partial hydration to not hydrate whole navbar straightaway.

radekdob avatar Jan 02 '25 18:01 radekdob

@ejthan hi, have you found any solution?

My current workaround with incremental hydration is to wrap components with used query with @defer(hydrate on x) and app is getting stable mode.

radekdob avatar Feb 05 '25 21:02 radekdob

@radekdob unfortunately i haven't found a good solution. We use this for an e-commerce application currently on Angular 18 and hydration is only working with zoneJs. we also can't defer those components as their content is seo relevant...

ejthan avatar Jun 30 '25 13:06 ejthan