Is it possible to prevent `useLazyQuery` queries from being re-fetched on component state change / re-render?
Asked Answered
A

4

27

Currently I have a useLazyQuery hook which is fired on a button press (part of a search form).

The hook behaves normally, and is only fired when the button is pressed. However, once I've fired it once, it's then fired every time the component re-renders (usually due to state changes).

So if I search once, then edit the search fields, the results appear immediately, and I don't have to click on the search button again.

Not the UI I want, and it causes an error if you delete the search text entirely (as it's trying to search with null as the variable), is there any way to prevent the useLazyQuery from being refetched on re-render?

This can be worked around using useQuery dependent on a 'searching' state which gets toggled on when I click on the button. However I'd rather see if I can avoid adding complexity to the component.

const AddCardSidebar = props => {
  const [searching, toggleSearching] = useState(false);
  const [searchParams, setSearchParams] = useState({
    name: ''
  });
  const [searchResults, setSearchResults] = useState([]);
  const [selectedCard, setSelectedCard] = useState();

  const [searchCardsQuery, searchCardsQueryResponse] = useLazyQuery(SEARCH_CARDS, {
    variables: { searchParams },
    onCompleted() {
      setSearchResults(searchCardsQueryResponse.data.searchCards.cards);
    }
  });

  ...

  return (
    <div>
      <h1>AddCardSidebar</h1>
      <div>
        {searchResults.length !== 0 &&
          searchResults.map(result => {
            return (
              <img
                key={result.scryfall_id}
                src={result.image_uris.small}
                alt={result.name}
                onClick={() => setSelectedCard(result.scryfall_id)}
              />
            );
          })}
      </div>
      <form>

        ...

        <button type='button' onClick={() => searchCardsQuery()}>
          Search
        </button>
      </form>

      ...

    </div>
  );
};
Abrupt answered 14/8, 2019 at 17:37 Comment(1)
Even I am getting this problem. What I am trying to do is useLazyQuery during the component mounting useEffect(() => refetchFunction(), []) But whenever my state changes, the refetchFunction is called again (INTERNALLY SOMEWHERE)Forcefeed
G
28

The react-apollo documentation doesn't mention whether useLazyQuery should continue to fire the query when variables change, however they do suggest using the useApolloClient hook when you want to manually fire a query. They have an example which matches this use case (clicking a button fires the query).

function DelayedQuery() {
  const [dog, setDog] = useState(null);
  const client = useApolloClient();

  return (
    <div>
      {dog && <img src={dog.displayImage} />}
      <button
        onClick={async () => {
          const { data } = await client.query({
            query: GET_DOG_PHOTO,
            variables: { breed: 'bulldog' },
          });
          setDog(data.dog);
        }}
      >
        Click me!
      </button>
    </div>
  );
}
Gottfried answered 15/8, 2019 at 8:35 Comment(1)
This solution perfectly resolved the issue I had with useLazyQuery which keeps firing requests on rerendering. Thank youIncontrollable
P
44

You don't have to use async with the apollo client (you can, it works). But if you want to use useLazyQuery you just have to pass variables on the onClick and not directly on the useLazyQuery call.

With the above example, the solution would be:

function DelayedQuery() {
  const [dog, setDog] = useState(null);
  const [getDogPhoto] = useLazyQuery(GET_DOG_PHOTO, {
    onCompleted: data => setDog(data.dog)
  })

  return (
    <div>
      {dog && <img src={dog.displayImage} />}
      <button
        onClick={() => getDogPhoto({ variables: { breed: 'bulldog' }})}
      >
        Click me!
      </button>
    </div>
  );
}
Putrid answered 22/1, 2020 at 14:9 Comment(4)
In my opinion this should be the accepted answer, since is using the useLazyQuery hook.Economy
How would you get back the error and loading states of the lazy query?Tag
@AndrewEinhorn - with const [getDogPhoto, {data, loading, error}] = useLazyQuery(GET_DOG_PHOTO...Balls
This works! useLazyQuery not getting fired again on component load/re-render. Thanks @Yann PravoEver
G
28

The react-apollo documentation doesn't mention whether useLazyQuery should continue to fire the query when variables change, however they do suggest using the useApolloClient hook when you want to manually fire a query. They have an example which matches this use case (clicking a button fires the query).

function DelayedQuery() {
  const [dog, setDog] = useState(null);
  const client = useApolloClient();

  return (
    <div>
      {dog && <img src={dog.displayImage} />}
      <button
        onClick={async () => {
          const { data } = await client.query({
            query: GET_DOG_PHOTO,
            variables: { breed: 'bulldog' },
          });
          setDog(data.dog);
        }}
      >
        Click me!
      </button>
    </div>
  );
}
Gottfried answered 15/8, 2019 at 8:35 Comment(1)
This solution perfectly resolved the issue I had with useLazyQuery which keeps firing requests on rerendering. Thank youIncontrollable
L
2

When using useLazyQuery, you can set nextFetchPolicy to "standby". This will prevent the query from firing again after the first fetch. For example, I'm using the hook inside of a Cart Context to fetch the cart from an E-Commerce Backend on initial load. After that, all the updates come from mutations of the cart.

Lampedusa answered 29/1, 2022 at 20:30 Comment(0)
S
1

The Apollo Client documentation isn't explicit about this, but useLazyQuery, like useQuery, fetches from the cache first. If there is no change between queries, it will not refetch using a network call. In order to make a network call each time, you can change the fetchPolicy to network-only or cache-and-network depending on your use case (documentation link to the fetchPolicy options). So with a fetchPolicy change of network-only in your example, it'd look like this:

const AddCardSidebar = props => {
    const [searching, toggleSearching] = useState(false);
    const [searchParams, setSearchParams] = useState({
         name: ''
    });
    const [searchResults, setSearchResults] = useState([]);
    const [selectedCard, setSelectedCard] = useState();

    const [searchCardsQuery, searchCardsQueryResponse] = 
        useLazyQuery(SEARCH_CARDS, {
            variables: { searchParams },
            fetchPolicy: 'network-only', //<-- only makes network requests
            onCompleted() {
            setSearchResults(searchCardsQueryResponse.data.searchCards.cards);
        }
    });
  ...

  return (
      <div>
      <h1>AddCardSidebar</h1>
      <div>
        {searchResults.length !== 0 &&
          searchResults.map(result => {
            return (
              <img
                key={result.scryfall_id}
                src={result.image_uris.small}
                alt={result.name}
                onClick={() => setSelectedCard(result.scryfall_id)}
              />
            );
        })}
      </div>
      <form>

        ...

        <button type='button' onClick={() => searchCardsQuery()}>
        Search
        </button>
      </form>

      ...

    </div>
  );
};
Stingo answered 12/8, 2021 at 16:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.