26

I've started playing with React-Query and it works great if I only need to fetch data from a single collection in my database. However, I'm struggling to find a good way to query multiple collections for use in a single component.

One Query (no problem):

const { isLoading, isError, data, error } = useQuery('stuff', fetchStuff)

if (isLoading) {
     return <span>Loading...</span>
   }
 
   if (isError) {
     return <span>Error: {error.message}</span>
   }
 
   return (
     <ul>
       {data.map(stuff => (
         <li key={stuff.id}>{stuff.title}</li>
       ))}
     </ul>
   )
 }

Multiple Queries to Different Collections (😬):

const { isLoading: isLoadingStuff, isError: isErrorStuff, data: stuff, error: errorStuff } = useQuery('stuff', fetchStuff);
const { isLoading: isLoadingThings, isError: isErrorThings, data: Things, error: errorThings } = useQuery('things', fetchThings);
const { isLoading: isLoadingDifferentStuff, isError: isErrorDifferentStuff, data: DifferentStuff, error: errorDifferentStuff } = useQuery('DifferentStuff', fetchDifferentStuff);

const isLoading = isLoadingStuff || isLoadingThings || isLoadingDifferentStuff
const isError = isErrorStuff || isErrorThings || isErrorDifferentStuff
const error = [errorStuff, errorThings, errorDifferentStuff]

if (isLoading) {
     return <span>Loading...</span>
   }
 
if (isError) {
    return (
      <span>
        {error.forEach((e) => (e ? console.log(e) : null))}
        Error: see console!
      </span>
    );
  }
 
   return (
  <>
    <ul>
      {stuff.map((el) => (
        <li key={el.id}>{el.title}</li>
      ))}
    </ul>
    <ul>
      {things.map((el) => (
        <li key={el.id}>{el.title}</li>
      ))}
    </ul>
    <ul>
      {differentStuff.map((el) => (
        <li key={el.id}>{el.title}</li>
      ))}
    </ul>
  </>
);
 }

I'm sure there must be a better way to do this. I'm very interested in React-Query for multiple reasons but one nice benefit is to reduce boilerplate. However, this approach doesn't seem much better than using useEffect and useState to manage my api calls. I did find the useQueries hook but it didn't make this any cleaner really.

Does anyone know if there is a way in React-Query to make multiple queries and only get back one isLoading, isError, and error(array?) response? Or just a better way to handle multiple queries that I'm missing?

6 Answers 6

28

I did find the useQueries hook but it didn't make this any cleaner really.

useQueries gives you an Array of results, so you can map over them:

const isLoading = queryResults.some(query => query.isLoading)

If you have a component that fires multiple concurrent requests, there is only so much the library can do to reduce complexity. Each query can have it's own loading state / error state / data. Each query can have its own settings and can behave differently. The recommended approach is still to extract that to a custom hook and return what you want from it.

error handling could be streamlined by using error boundaries with the useErrorBoundary option. To streamline loading experience, you can try suspense (though experimental), it will show the fallback loader for all queries.

this approach doesn't seem much better than using useEffect and useState to manage my api calls.

this is ignoring all the advantages like (amongst others) caching, background refetches, mutations, smart invalidation etc.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks TkDodo! I'm still very new to all this and I did not know about the array.some() method. I'll also look into error boundaries.
12

With Dependent Queries you can follow the documentation's example.

const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects

const { isIdle, data: projects } = useQuery(
  ['projects', userId],
  getProjectsByUser,
  {
    // The query will not execute until the userId exists
    enabled: !!userId,
  }
 )

Official documentation - Dependent Queries

1 Comment

question is not for dependent queries
11

There are two options to do so:

  • Either use useQueries hook in case the queries are independent of each other.
import React from 'react';
import { useQueries } from '@tanstack/react-query';
import axios from 'axios';

export default function Example() {
  const [postsQuery, usersQuery] = useQueries({
    queries: [
      {
        queryKey: ['posts'],
        queryFn: () =>
          axios
            .get('https://jsonplaceholder.typicode.com/posts')
            .then((res) => res.data),
      },

      {
        queryKey: ['users'],
        queryFn: () =>
          axios
            .get('https://jsonplaceholder.typicode.com/users')
            .then((res) => res.data),
      },
    ],
  });

  if (postsQuery.isLoading) return 'Loading Posts...';
  if (usersQuery.isLoading) return 'Loading Users...';

  if (postsQuery.error)
    return 'An error has occurred: ' + postsQuery.error.message;

  if (usersQuery.error)
    return 'An error has occurred: ' + usersQuery.error.message;

  return (
    <div>
      <h2>Posts</h2>
      {postsQuery.data?.map((post) => {
        return (
          <div key={post.id} style={{ display: 'flex' }}>
            <span>{post.id}-&nbsp;</span>
            <div>{post.title}</div>
          </div>
        );
      })}

      <h2>Users</h2>
      {usersQuery.data?.map((user) => {
        return (
          <div key={user.id} style={{ display: 'flex' }}>
            <span>{user.id}-&nbsp;</span>
            <div>{user.name}</div>
          </div>
        );
      })}
    </div>
  );
}

  • OR, using the dependent queries option, by adding the enabled property.
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

export default function Example() {
  const {
    isLoading: loadingPost,
    error: errorPost,
    data: postData,
  } = useQuery(['post', 1], () =>
    axios
      .get('https://jsonplaceholder.typicode.com/posts/1')
      .then((res) => res.data)
  );

  const {
    isLoading: loadingPostComments,
    error: errorPostComments,
    data: commentsData,
  } = useQuery(
    ['comments', 'post', 1],
    () =>
      axios
        .get('https://jsonplaceholder.typicode.com/posts/1/comments')
        .then((res) => res.data),
    {
      enabled: postData && Object.keys(postData).length > 0,
    }
  );

  if (loadingPost) return 'Loading Posts...';
  if (errorPost) return 'An error has occurred: ' + errorPost.message;

  if (loadingPostComments) return 'Loading Comments...';
  if (errorPostComments)
    return 'An error has occurred: ' + errorPostComments.message;

  return (
    <div>
      <h2>Post</h2>
      {postData && (
        <div key={postData.id} style={{ display: 'flex' }}>
          <span>{postData.id}-&nbsp;</span>
          <div>{postData.title}</div>
        </div>
      )}

      <h2>-- Comments</h2>
      {commentsData?.map((comment) => {
        return (
          <div key={comment.id} style={{ display: 'flex' }}>
            <span>{comment.id}-&nbsp;</span>
            <div>{comment.body}</div>
          </div>
        );
      })}
    </div>
  );
}

You can find more info on this link.

Comments

4

You can abstract your queries into individual query files and then make custom hooks for each collection of data you want to fetch together (most likely, collections of queries needed to render a single page)

// ../queries/someQuery.js
export const useContactInformation = () => {

  const { data, isLoading, error } = useQuery(CONTACT_INFORMATION, () => apicall(someparams), {
    enabled: !!user?.id,
  });

  return {
    contactInformation: data
    isLoading,
    error,
  };
};

and then in another file...

// ../hooks/somepage.js

export const useSomePageData = () => {
  const { contactInformation, error: contactError, isLoading: contactIsLoading } = useContactInformation();
  const { activityList, error: activityError, completedActivity, isLoading: activityIsLoading } = useActivityList();

  # return true, only when all queries are done
  const isLoading = activityIsLoading || contactIsLoading;
  # return true, only when all queries run successfully
  const error = activityError || contactError;

  return {
    error,
    isLoading,
    activityList,
  };
};

Comments

0

as leveraging useQueries is the best solution, the following is another approach to make it all easier to maintain:

const useMultipleData = () => {

// Say we have two API Fetching Functions: fetchPosts and fetchComments 
const queries = useQueries([
    { queryKey: 'posts', queryFn: fetchPosts },
    { queryKey: 'comments', queryFn: fetchComments },
  ]);

  return {
    posts: queries[0].data,
    isPostsLoading: queries[0].isLoading,
    postsError: queries[0].error,
    comments: queries[1].data,
    isCommentsLoading: queries[1].isLoading,
    commentsError: queries[1].error,
  };
};

Then use it like so:

const {
    posts,
    isPostsLoading,
    postsError,
    comments,
    isCommentsLoading,
    commentsError,
  } = useMultipleData();

Comments

0

use custom hooks

const useProductAdd = () => {
 const { mutate, isSuccess, isLoading, isError, data } = useFirst();
 const add = useSecound();

 useEffect(() => {
  if (!isSuccess) return;
  add.mutate({ payload: data?.payload, id: data?.res?.data?.id });
 }, [add, data, isSuccess]);

 return { 
    ...add,
    isLoading: isLoading || add.isLoading,
    isError: isError || add.isError,
    mutate,
   };
};

export default useProductAdd;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.