T
TanStack3y ago
absent-sapphire

`suspense: true` is not triggering Suspense

Hello, In my codebase I have the following code:
export function useUser() {
const query = useQuery({
queryKey: ['auth', 'useUser'],
queryFn: async () => {
// do some stuff
},
suspense: true,
networkMode: 'offlineFirst',
cacheTime: ms('1 day'),
staleTime: ms('1 hour'),
});

if (!query.data) {
throw new Error('Suspense not called on useUser');
}

return query;
}
export function useUser() {
const query = useQuery({
queryKey: ['auth', 'useUser'],
queryFn: async () => {
// do some stuff
},
suspense: true,
networkMode: 'offlineFirst',
cacheTime: ms('1 day'),
staleTime: ms('1 hour'),
});

if (!query.data) {
throw new Error('Suspense not called on useUser');
}

return query;
}
However it throws Suspense not called on useUser. I thought the way that suspense worked is that useQuery itself would throw an object that would be caught by the parent Suspense component and would not continue to the next line until the request is completed. Do I misunderstand?
11 Replies
absent-sapphire
absent-sapphireOP3y ago
I have a parent <Suspense but not <ErrorBoundary, not sure if I need one More context:
<Suspense fallback={<SuspenseFallback />}>
<Modals>
<Router />
<Feedback />
<DevSettings />
</Modals>
</Suspense>
<Suspense fallback={<SuspenseFallback />}>
<Modals>
<Router />
<Feedback />
<DevSettings />
</Modals>
</Suspense>
error message:
Uncaught Error: Suspense not called on useUser
at useUser (useUser.tsx:88:11)
at Feedback (Feedback.tsx:7:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20074:13)
at beginWork (react-dom.development.js:21587:16)
at HTMLUnknownElement.callCallback (react-dom.development.js:4164:14)
at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:16)
at invokeGuardedCallback (react-dom.development.js:4277:31)
at beginWork$1 (react-dom.development.js:27451:7)
at performUnitOfWork (react-dom.development.js:26557:12)
Uncaught Error: Suspense not called on useUser
at useUser (useUser.tsx:88:11)
at Feedback (Feedback.tsx:7:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20074:13)
at beginWork (react-dom.development.js:21587:16)
at HTMLUnknownElement.callCallback (react-dom.development.js:4164:14)
at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:16)
at invokeGuardedCallback (react-dom.development.js:4277:31)
at beginWork$1 (react-dom.development.js:27451:7)
at performUnitOfWork (react-dom.development.js:26557:12)
More error message:
react-dom.development.js:18687 The above error occurred in the <Feedback> component:

at Feedback (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:3738:39)
at InnerContextProvider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42747:23)
at Provider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42756:23)
at Modals (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42379:13)
at Suspense
at QueryClientProvider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:34698:32)
at PersistQueryClientProvider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:43183:39)
at ReactQuery (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42941:13)
at Suspense
at div
at Document (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:3539:13)
at IndexNewTab

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
react-dom.development.js:18687 The above error occurred in the <Feedback> component:

at Feedback (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:3738:39)
at InnerContextProvider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42747:23)
at Provider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42756:23)
at Modals (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42379:13)
at Suspense
at QueryClientProvider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:34698:32)
at PersistQueryClientProvider (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:43183:39)
at ReactQuery (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:42941:13)
at Suspense
at div
at Document (chrome-extension://pceplcooabdibmdecolmlefkiomdpabe/newtab.7ee8b14b.js:3539:13)
at IndexNewTab

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
And console.logging the query before the throw
{
"status": "loading",
"fetchStatus": "idle",
"isLoading": true,
"isSuccess": false,
"isError": false,
"isInitialLoading": false,
"dataUpdatedAt": 0,
"error": null,
"errorUpdatedAt": 0,
"failureCount": 0,
"failureReason": null,
"errorUpdateCount": 0,
"isFetched": false,
"isFetchedAfterMount": false,
"isFetching": false,
"isRefetching": false,
"isLoadingError": false,
"isPaused": false,
"isPlaceholderData": false,
"isPreviousData": false,
"isRefetchError": false,
"isStale": true
}
{
"status": "loading",
"fetchStatus": "idle",
"isLoading": true,
"isSuccess": false,
"isError": false,
"isInitialLoading": false,
"dataUpdatedAt": 0,
"error": null,
"errorUpdatedAt": 0,
"failureCount": 0,
"failureReason": null,
"errorUpdateCount": 0,
"isFetched": false,
"isFetchedAfterMount": false,
"isFetching": false,
"isRefetching": false,
"isLoadingError": false,
"isPaused": false,
"isPlaceholderData": false,
"isPreviousData": false,
"isRefetchError": false,
"isStale": true
}
So it definitely should be calling parent Suspense right? Unless I misunderstand how it works
xenial-black
xenial-black3y ago
fetchStatus is idle so it's not suspending. Are you using enabled or a persist plugin?
absent-sapphire
absent-sapphireOP3y ago
I'm using persist So looks like this is why: https://github.com/TanStack/query/blob/512d059a03f85289edf7e56bccc1ce5f050b5c90/packages/react-query/src/suspense.ts
export function ReactQuery(props: PropsWithChildren<unknown>) {
const { children } = props;
const [loading, setLoading] = useState(true);

return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={persistOptions}
onSuccess={() => {
setLoading(false);
}}
>
{loading ? <SuspenseFallback /> : children}
</PersistQueryClientProvider>
);
}
export function ReactQuery(props: PropsWithChildren<unknown>) {
const { children } = props;
const [loading, setLoading] = useState(true);

return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={persistOptions}
onSuccess={() => {
setLoading(false);
}}
>
{loading ? <SuspenseFallback /> : children}
</PersistQueryClientProvider>
);
}
Making this change at the top level seems to fix it (SuspenseFallback is just my generic loading page), is this the right way to go about it?
xenial-black
xenial-black3y ago
It's up to you what to display in this case. We don't put the query in loading state because after the restore from persistence is done, the query might have fresh data which doesn't require a loading state
absent-sapphire
absent-sapphireOP3y ago
I mean i just want to not render my app until the data is fetched from local storage, this seems to be the best way to do that as far as I can see. This works fine, but as a thought experiment, what do you consider the state when persistence is loading to be? I would consider that to be loading (trigger suspense) since react-query probably can't do anything until that is done
xenial-black
xenial-black3y ago
You can read via useIsRestoring and choose to not render your app until we're done, that's fine. We don't do that per default because it would ruin SSR
xenial-black
xenial-black3y ago
persistQueryClient | TanStack Query Docs
This is set of utilities for interacting with "persisters" which save your queryClient for later use. Different persisters can be used to store your client and cache to many different storage layers. Build Persisters
xenial-black
xenial-black3y ago
It's in status:loading and fetchStatus:idle. Since the queryFn is not running, we're not suspending.
absent-sapphire
absent-sapphireOP3y ago
Fair enough, my intuition was initially that the isLoading is what triggers a suspense as opposed to fetching. Here is a trickier issue I'm coming across that I can't seem to put my finger on. So when my device is connected to the internet my (other) code works, however when my device is offline, I get the following error: Uncaught Error: Suspense not called for storage.dashboards This is after I added the onSuccess clause above. Here is the following code:
const query = useQuery({
queryKey,
queryFn: async () => {
const store = await chromeLocalStorage.get<Data<T>>(storageKey);
const data = normalizeData(store);
version.current = data.version;
child.debug('Fetching data from storage', { storageKey, store, data });
return data.data;
},
suspense: true,
meta: {
storable: false,

});

if (!query.data) throw new Error(`Suspense not called for ${KEY.join('.')}`);
const query = useQuery({
queryKey,
queryFn: async () => {
const store = await chromeLocalStorage.get<Data<T>>(storageKey);
const data = normalizeData(store);
version.current = data.version;
child.debug('Fetching data from storage', { storageKey, store, data });
return data.data;
},
suspense: true,
meta: {
storable: false,

});

if (!query.data) throw new Error(`Suspense not called for ${KEY.join('.')}`);
What is odd is that it works when connected to the internet, but not when it's offline. Maybe it has to do with the meta.storable code (copied from docs). I think the onSuccess callback code above stops this from rendering until the cache is loaded from localStorage but maybe react-query detects that the network is offline and it's not available in the storage (because of storable: false). Not sure if theres a way for me to tell react query to try this query even when the network is offline since it doesnt actually make a network request. I do not see a "Fetching data from storage" log anywhere
xenial-black
xenial-black3y ago
Set networkMode:'always'
absent-sapphire
absent-sapphireOP3y ago
Thanks! Sorry for bugging you with so many simple questions

Did you find this page helpful?