go back

You probably don't need useLazyQuery 🙅‍♂️

March 23, 2021

My team maintains a pretty simple (React/Apollo) page that displays a list of items filtered and sorted by various values with corresponding inputs. To reduce database load and eliminate an unused use case, we decided to only query for the list when at least one filter has a value. This meant moving away from fetching via Apollo's useQuery hook on every render.

At first the obvious solution appeared to be to swap out the useQuery hook for useLazyQuery. But since the page has no explicit "Search" button (changing a filter automatically re-queries with updated params), an effect would be needed to trigger the now-lazy query.

After setting this up, it didn't feel right and I realized it was because this went against the patterns that hooks were designed to encourage. So to append to the clickbait-y title, you probably don't need it if your query isn't triggered by an explicit user interaction or event.


Apollo's useLazyQuery provides a function to trigger the query on-demand. This is the purpose of laziness; don't do something now, but maybe do it later. But React already has a more elegant way of controlling this behavior: conditional rendering.

Take this component example, using useLazyQuery (the initial approach I mentioned):

import React, { useState, Fragment } from 'react';
import { useLazyQuery } from 'react-apollo';

const Menu = () => {
    const [food, setFood] = useState('pizza');
    const [search, { data, error, loading }] = useLazyQuery(
        GET_INGREDIENTS,
        { variables: { food } }
    );

    useEffect(() => {
        const shouldSearch = food !== 'pizza';

        if (shouldSearch) {
            search();
        }
    }, [food]);

    return (
        <Fragment>
            <input type='text' onChange={setFood} />
            <Ingredients data={data || []} />
        </Fragment>
    );
};

const Ingredients = ({ data }) => data.map(({ name, description }) => (
    <div key={name}>
        <span>{description}</span>
    </div>
));
Enter fullscreen mode Exit fullscreen mode

This code works, although it obscures logic in a useEffect that could be hard to find later on. As a component naturally grows in complexity over time, it's important to keep logic like this organized and written as concise as possible so that side-effects don't become a black box.

A simpler approach would be to refactor the useQuery logic into into another component and have it only exist when we want it to:

const Menu = () => {
    const [food, setFood] = useState('pizza');

    return (
        <Fragment>
            <input type='text' onChange={setFood} />
            <If condition={food !== 'pizza'}>
                <Ingredients food={food} />
            </If>
        </Fragment>
    );
};

const Ingredients = ({ food }) => {
    const { data, error, loading } = useQuery(
        GET_INGREDIENTS,
        { variables: { food } }
    );


    if (error || loading) {
        return null;
    }

    return data.map(({ name, description }) => (
        <div key={name}>
            <span>{description}</span>
        </div>
    ));
};
Enter fullscreen mode Exit fullscreen mode
The If syntax comes from the jsx-control-statements package, they're pretty nifty for readability in situations like this.

This is better! ✨

Now, when looking for why a query only occurs in certain states I can see the clear, declarative logic in the render instead of needing to dig through the rest of the component.

Again, if the query you need to become lazier is triggered by a user interaction, chances are useLazyQuery is the way to go. But if you're sticking to the hook mentality of letting everything be driven by state, organizing your components like this could help keep your code easy to follow!

go back