17

I am using React-query for my API calls. I am wondering if there is a way to call the query in a lazy way.

Meaning to call the query only when a query param changes.

This is what I currently have; I am using a hack with the useEffect where if recipeName changes, then run the refetch function.

export const searchRecipeByName = async (recipeName: string) => {
  return await api.get(
    `/recipes/complexSearch?apiKey=${process.env.NEXT_PUBLIC_SPOONACULAR_API_KEY}&query=${recipeName}&addRecipeInformation=true&fillIngredients=true`
  );
};
  const [recipeName, setRecipeName] = useState("");

  const { data, refetch } = useQuery(
    "homePageSearchQuery",
    () => searchRecipeByName(recipeName),
    { enabled: false }
  );

// HACK
  useEffect(() => {
    if (!!recipeName) {
      refetch();
    }
  }, [recipeName]);

  const handleOnSearchSubmit = async (recipeSearch: RecipeSearch) => {
    setRecipeName(recipeSearch.search);
  };

Preferably, I would like to call the query in the handleOnSearchSubmit function.

I could create a custom useLazyQuery hook to handle this, but I'm wondering if React-query has a native way to handle this.

4 Answers 4

18

Preferably, I would like to call the query in the "handleOnSearchSubmit" function.

This is a quite imperative way of thinking about things, but react-query is more declarative. You don't specify: "when I click that button, I want to fetch", but you just say: "these are the inputs I require" and react-query will refetch whenever those inputs change.

So what you want to do is put the things that you need for your query to run into the query key, so that react-query

  • caches it separately for every dependency
  • you can disable it as long as dependencies are not ready
const [recipeName, setRecipeName] = React.useState('')
const { isLoading, data } = useQuery(
  ['homePageSearchQuery', recipeName],
  () => searchRecipeByName(recipeName),
  {
    enabled: !!recipeName
  }
])

then, all you need to do is call setRecipeName in handleOnSearchSubmit, and react-query will do the querying.

some more things that you might want to consider:

  • if you want to avoid a hard loading state in between recipeName changes, set keepPreviousData: true
  • the above code will run a query every time the recipeName changes. If that doesn't happen on a button click, but say whenever the user types something in an input field, you you want to debounce that. For those cases, consider something like useDebounce:
const [recipeName, setRecipeName] = React.useState('')
const debouncedRecipeName = useDebounce(recipeName, 1000)
const { isLoading, data } = useQuery(
  ['homePageSearchQuery', debouncedRecipeName],
  () => searchRecipeByName(debouncedRecipeName),
  {
    enabled: !!debouncedRecipeName
  }
])

This will let you input data and show it in a text field, but only creates a new cache entry / fires a new request after 1 second of inactivity.

  • if you want to have a button that submits the form, it's best to "lift state up": Have a more global state that the query depends on, and some local state for the form. Once the user hits "submit", you save the user selection in the state that lives higher up, which will trigger a query. I like to use the url for this, and I have a complete codesandbox example here. The actual form state is uncontrolled in that example, but you can also use state or a form lib for that
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks TKDodo for the extensive answer. It's a shame that React-query doesn't have a useLazyQuery natively like Apollo client. I will give this a shot
@TKDodo can you not disable the query initially then manually call refetch when needed?
sure, but then you opt out of all the smart refetches / invalidation that react-query provides. everything is "manual" at that point. see: tanstack.com/query/v4/docs/guides/disabling-queries
2

You can use this Wrapper hook to make useLazyQuery in react-query

import { useCallback, useState } from 'react';
import {
  QueryFunction,
  QueryKey,
  useQuery,
  UseQueryOptions,
  UseQueryResult,
} from 'react-query';

type UseQueryParams = Parameters<typeof useQuery>;

export default function useLazyQuery<TData, TError>(
  key: UseQueryParams[0],
  fetchFn: QueryFunction<TData, QueryKey>,
  options?: Omit<UseQueryOptions<TData, TError, unknown, QueryKey>, 'queryKey' | 'queryFn'>
): [() => void, UseQueryResult<unknown, unknown>] {
  const [enabled, setEnabled] = useState(false);

  const query = useQuery<TData, TError, unknown, QueryKey>(key, fetchFn, {
    ...(options || {}),
    enabled,
  });

  const trigger = useCallback(() => {
    if (!enabled) {
      setEnabled(true);
    }
  }, [fetchFn, enabled]);

  return [trigger, query];
}

Comments

2

Lazy query can be implemented the following way:

import { useQueryClient } from '@tanstack/react-query';

export const useGetRecipeByNameLazy = () => {
  const queryClient = useQueryClient();
  return (recipeName: string) => queryClient.ensureQueryData({
    queryKey: ['homePageSearchQuery', recipeName],
    queryFn: () => searchRecipeByName(recipeName),
  });
};

The usage:

const getRecipeByName = useGetRecipeByNameLazy();

const handleSearch = async (recipeName: string) => {
    const recipe = await getRecipeByName(recipeName);
    console.log(recipe);
}

Comments

0

Do it like this

const { isLoading, data } = useQuery([
    'homePageSearchQuery',
    {recipeName}],
    () => searchRecipeByName(recipeName), {}
);

It will run the query whenever the value of recipeName changes.

7 Comments

Hi Ibrahim, I tried that initially, but it isn't working. I also tried calling the refetch function in my "handleOnSearchSubmit" function. But refetch doesn't take in the query string param
Ok, removing the enable does work. But that seems to fire the query on every rerender as well
Glad that it is working, since you initially had set enabled value to false, it was stopping the hook from firing the fetch query. About re-render you need to check what is causing multiple re-render and take care of it.
If you are using it for search, I will also suggest you to use debounce when using the query. I personally use debounce function from lodash
Thanks for that, it is used for search, but it's doing a search on a button click. But is there no way of only running the query only when the "handleOnSearchSubmit" is called? In Apollo client, there is the useLazyQuery hook natively.
|

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.