-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
suspense is not working for useQueries #1523
Comments
Confirmed. We'll look into this. |
Hi, I just wondered if there is an ETA for this bug to be looked at? I'm currently using a hacky workaround but it would be great to get a proper fix. Many thanks for the library. |
Im interested in what your workaround is....I came up with one as well but it doenst work in a new use case i have. |
Hi. My workaround is to create an async function which calls multiple APIs at once using Promise.all. And then call this function from React Query. It works but misses out on a lot of the power of React Query if each api call was handled separately. |
Hi, sorry for necrobumping this issue, but for people who are looking for a workaround (until v4 is released) while keeping all of the power of react-query, we think we've came up with an acceptable solution. useQuery does suspend but you can't dynamically create useQuery hook calls, what you can dynamically create, though, is components which use useQuery. For each query in useQueries you can dynamically create a component, which calls useQuery with the provided query and renders nothing. To avoid manually having to create it, here's our solution using Context API. Let's assume you have a custom component which you use to wrap your component tree with Suspense: export const SuspenseWithBoundary = forwardRef(({ children, SuspenseProps = {}, ErrorBoundaryProps = {} }, ref) => {
return (
<ErrorBoundary
ref={ref}
onError={error => console.log(error.message)}
fallbackRender={({ error }) => {
return (
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
{error.message}
</Alert>
);
}}
{...ErrorBoundaryProps}
>
<Suspense fallback={<LoadingSpinner />} {...SuspenseProps}>
{children}
</Suspense>
</ErrorBoundary>
);
}); Let's create a context and a component which will receive a list of queries and then renders them under the Suspense component: export const SuspendQueriesContext = createContext({
addQueries: (id, queries) => {},
removeQueries: id => {}
}); export const QueriesSuspender = ({ children }) => {
// Store queries in a map where keys are IDs of the querying hooks
const [queriesMap, setQueriesMap] = useState({});
const addQueries = useCallback((id, queries) => {
setQueriesMap(prevQueriesMap => ({
...prevQueriesMap,
[id]: queries
}));
}, []);
const removeQueries = useCallback(id => {
setQueriesMap(prevQueriesMap => {
const newQueriesMap = { ...prevQueriesMap };
delete newQueriesMap[id];
return newQueriesMap;
});
}, []);
const value = useMemo(
() => ({
addQueries,
removeQueries
}),
[addQueries, removeQueries]
);
return (
<SuspendQueriesContext.Provider value={value}>
{children}
{Object.entries(queriesMap).map(([id, queries]) =>
queries.map((query, index) => <QueryWrapper key={`${id}-${index}`} query={query} />)
)}
</SuspendQueriesContext.Provider>
);
}; The QueryWrapper component looks like this: export const QueryWrapper = ({ query }) => {
useQuery(query);
return null;
}; Add your newly created component to your custom Suspense component: export const SuspenseWithBoundary = forwardRef(({ children, SuspenseProps = {}, ErrorBoundaryProps = {} }, ref) => {
return (
<ErrorBoundary
ref={ref}
onError={error => console.log(error.message)}
fallbackRender={({ error }) => {
return (
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
{error.message}
</Alert>
);
}}
{...ErrorBoundaryProps}
>
<Suspense fallback={<LoadingSpinner />} {...SuspenseProps}>
<QueriesSuspender>{children}</QueriesSuspender>
</Suspense>
</ErrorBoundary>
);
}); To add and remove queries which are to be rendered let's create a hook: let generator = 0;
export const useSuspendQueries = queries => {
// Each time queries change, generate a new ID. The reason for generating a new one once queries change is that
// we can uniquely identify each query when rendering it in QueriesSuspender component.
// eslint-disable-next-line react-hooks/exhaustive-deps
const id = useMemo(() => ++generator, [queries]);
const { addQueries, removeQueries } = useContext(SuspendQueriesContext);
// When id and queries change (they change at the same time), remove previous queries with previous ID and add
// new queries with a new ID. useLayoutEffect is used to immediately mount the queries into the tree so the
// the component tree immediately suspends if the queries haven't been resolved yet.
useLayoutEffect(() => {
addQueries(id, queries);
return () => removeQueries(id);
}, [id, addQueries, removeQueries, queries]);
}; Finally, create on more hook which you will use instead of using useQueries: export const useSuspendingQueries = queries => {
useSuspendQueries(queries);
return useQueries(queries);
}; And this is how you use it, for example: const results = useSuspendingQueries(
useMemo(() => {
return (
targets?.map(target => {
const queryKey = getMethodsQueryKey(target.id);
return {
queryKey,
queryFn: () => axiosGet(queryKey)
};
}) || []
);
}, [targets])
); The API is identical to the one useQueries uses, you don't have to add any other code. The mechanism automatically creates components which call useQuery for each query passed in as a parameter in the useSuspendingQueries and suspends the tree. Keep in mind, thought, that the array of queries passed should be memoized. |
I don't think this can really work as we intended it to work with Basically, what we wanted to do is have multiple queries fire off at the same time, and then suspend until at least one (or all) have returned. The solution with multiple
This goes a lot against what I really like the ideas that @lubieowoce has had in that regard, and I'm hoping that she will find the time to contribute that. It's likely not easy to implement, especially the mixed version where some queries suspend and some don't. The new useQueries api with the object syntax is merged already to v4, so I think everything that we'll add in that regard can be made backwards compatible, given that |
Hi, thanks for the response. I wouldn't say it leads to a waterfall. The reason is that apart from using useQuery, you use useQueries as well. Even if the first mounted component suspends, useQueries still fires off the remaining queries. It keeps mounting the useQuery components - if it arrives at one which hasn't been fetched it suspends and the process keeps repeating unless all of the queries are resolved. Here's a screenshot from devtools. We firstly fetch a list of targets, then for each target its list of methods and for each method its list of reactions. I wouldn't say there's a waterfall. I am not saying this solution is ideal, it's just a temporary workaround which we found working for us fairly well and wanted to share with others if they are looking for one. I suspect there might be a slight performance hit doing it this way. |
Hello, I wrote drop in replacement for I hope someone can refactor code and make PR.
|
It took quite some time but I think I have a working version. Please feel free to try it out and provide feedback. It's sill experimental, so don't expect too much 😅 |
@TkDodo are you planning to trigger suspense with isFetching state of a query? |
No |
Describe the bug
To Reproduce
Expected behavior
useQueries should works like the useQuery loop, the result status should be success
Screenshots
Desktop (please complete the following information):
Additional context
The text was updated successfully, but these errors were encountered: