18

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?

Beadle
  • 389
  • 1
  • 2
  • 11

4 Answers4

22

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.

TkDodo
  • 20,449
  • 3
  • 50
  • 65
  • 1
    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. – Beadle Mar 02 '21 at 15:05
8

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

Banzy
  • 1,590
  • 15
  • 14
5

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.

M1M6
  • 915
  • 3
  • 17
  • 33
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,
  };
};
ambe5960
  • 1,870
  • 2
  • 19
  • 47