How to create a quick search component using Apollo lazy query

Marcin Kwiatkowski /
How to create a quick search component using Apollo lazy query

When React renders a component that calls the useQuery hook, the Apollo Client runs the query automatically, but sometimes you need to query for data on some event. A great case is a quick search component that allows users to search for products in an eCommerce store.

There is a useLazyQuery hook that returns a query function. You can use this function wherever you want, and when you fire it, Apollo will execute a query for you.

When would you use lazy queries?

Typically custom Apollo's hook useQuery is used when you want to query data. That kook is called when React mounts and renders the component, and Apollo Client automatically executes a query then.

What to do when you want to call a query manually thought? For instance, when a user clicks on a specific button, or if you're running a query in a useEffect function?

Lazy queries for the rescue!

In that case, you can use the useLazyQuery hook. As I mentioned earlier, It's pretty much the same as useQuery with one exception. When useLazyQuery is called, it does not immediately execute its associated query.

Instead, it returns a function in its result tuple that you can call whenever you're ready to execute the query.

On the other hand, the useLazyQuery hook is ideal for executing queries in response to different events, such as user actions. Whenever the useLazyQuery command occurs, the application will not immediately run any queries. You need to trigger a query manually.

Take a look at the example:

1import React from 'react';
2import { useLazyQuery } from '@apollo/client';
3import { GET_SWEETIES } from './somewhere';
4
5const MySweeties = () => {
6  const [getSweeties, { loading, data }] = useLazyQuery(GET_SWEETIES);
7
8  if (loading) return <p>Loading ...</p>;
9
10  const shouldDisplaySweeties = data && data.sweeties ? 
11
12    <img src={data.sweeties.image} alt="sweeties" /> : <button onClick={() => getSweeties({ variables: { type: 'chocolate' } })}>
13    Get sweeties!
14  </button>
15
16  return shouldDisplaySweeties;
17}
18

How useful is that?

Refetch function

The useQuery returns data, and the useLazyQuery returns data as well. Those hooks return cached data or server data. It doesn't matter, they return the requested data and the component renders that data. The default behavior of the useQuery hook is to perform a query when the component re renders. Lazy query allows performing a query on demand.

Moreover, an additional mechanism allows to refetch queries on demand to particular user action. Take a look at the example:

1const { loading, error, data, refetch } = useQuery(YOUR_QERY, {    variables: { sampleVar: 'abc'},  });

You can bind refetch function in the JSX code like this:

1 <button onClick={() => refetch({ sampleVar: 'xyz' })}>Refetch</button>

As you can see, you can pass new variables to the function.

Practical usage

Let's use that knowledge about lazy queries and implement a QuickSearch component in the React app.

QuickSearch component

Create a quick search component with the following content:

1import React, { useState } from 'react';
2import { Form, FormControl } from 'react-bootstrap';
3import QuickSearchSuggestions from '../QuickSearchSuggestions';
4
5
6const QuickSearch = () => {
7    const [ isValid, setIsValid] = useState(false);
8    const [ searchQuery, setSearchQuery ] = useState('');
9    const handleChange = value => {
10        const valueEntered = !!value;
11        const isValid = valueEntered && value.length > 3;
12
13        setIsValid(isValid);
14        setSearchQuery(value);
15    }
16
17    return (
18        <div className="justify-content-center d-flex position-relative">
19            <Form inline className="w-100">
20                <FormControl type="text" placeholder="Search entire shop" className="w-100" onChange={e => handleChange(e.target.value)}/>
21            </Form>
22            <QuickSearchSuggestions isValid={isValid} searchQuery={searchQuery}/>
23        </div>
24    );
25};
26
27export default QuickSearch;
28

I define two states there: isValid and searchQuery, and I pass them to the child component. I check the length of the value entered by a user in the search input, and if the length is higher than 3, I send the query.

Quick Search Suggestion component

Create a new component called QuickSearchSuggestion with the following content:

1import React from 'react';
2
3import { ListGroup } from 'react-bootstrap';
4import PropTypes from 'prop-types';
5import useQuickSearchSuggestions from '../../hooks/useQuickSearchSuggestions';
6import classes from './QuickSearchSuggestions.module.css'
7
8const QuickSearchSuggestions = (props) => {
9    const { isValid, searchQuery } = props;
10    const {
11        hasSuggestions,
12        isLoading,
13        isOpen,
14        items
15    } = useQuickSearchSuggestions({ isValid, searchQuery });
16
17    const suggestions = items.map(product => {
18        return <ListGroup.Item key={product.id}>
19            {product.name}
20        </ListGroup.Item>
21    });
22
23    const shouldDisplaySuggestions = suggestions ? <div className={classes.suggestions}>
24        <ListGroup>
25            {suggestions}
26        </ListGroup>
27    </div> : null;
28
29    if (isOpen && hasSuggestions) {
30        return shouldDisplaySuggestions;
31    } else if (isLoading) {
32        return <div className={classes.suggestions}>
33            <ListGroup.Item>Loading...</ListGroup.Item>
34        </div>;
35    } else if (isOpen && !hasSuggestions) {
36        return <div className={classes.suggestions}>
37            <ListGroup>
38                <ListGroup.Item>No products found</ListGroup.Item>
39            </ListGroup>
40        </div>
41    } else {
42        return null;
43    }
44};
45
46QuickSearchSuggestions.propTypes = {
47    isValid: PropTypes.bool.isRequired,
48    searchQuery: PropTypes.string.isRequired
49};
50
51export default QuickSearchSuggestions;
52

I use a custom hook there: useQuickSearchSuggestion. That hook's responsibility is to provide data and business logic for the component. I am going to define that hook in the next step. 

To finish the QuickSearchSuggestion, I create a CSS module QuickSearchSuggestions.module.css with the following styles:

1.suggestions {
2    left: 0;
3    position: absolute;
4    top: 100%;
5    right: 0;
6    z-index: 10;
7}
8

use Quick Search Suggestions custom hook

1import { useEffect, useState } from 'react';
2import { useLazyQuery } from '@apollo/client';
3import { GET_QUICK_SEARCH_SUGGESTIONS } from '../../queries/product.gql';
4
5/**
6 * The useQuickSearchSuggestions hook provides data and business logic for the QuickSearchSuggestions component
7 *
8 * @return {
9 *  hasSuggestions {bool} - determines are products found based on provided search query
10 *  isLoading {bool} - determines is data is currently loading
11 *  isOpen {bool} - determines is component is opened
12 *  items {array} - array with products returned from the API based on provided search query
13 * }
14 */
15export const useQuickSearchSuggestions = props => {
16    const { isValid, searchQuery } = props;
17    const [ isOpen, setIsOpen ] = useState(false);
18    const [ hasSuggestions, setHasSuggestions ] = useState(false);
19    const [fetchSuggestions, { loading, data }] = useLazyQuery(GET_QUICK_SEARCH_SUGGESTIONS);
20
21    useEffect(() => {
22        if (isValid) {
23            fetchSuggestions({
24                variables: {
25                    searchQuery
26                }
27            });
28            setIsOpen(true);
29        } else {
30            setIsOpen(false);
31        }
32    }, [fetchSuggestions, isValid, searchQuery]);
33
34    useEffect(() => {
35        data && data.products && data.products.items && data.products.items.length ?
36            setHasSuggestions(true): setHasSuggestions(false);
37    }, [data]);
38
39    return {
40        hasSuggestions,
41        isLoading: loading,
42        isOpen,
43        items: data && data.products && data.products.items ? data.products.items : []
44    }
45}
46
47export default useQuickSearchSuggestions;
48

As you can see, I've defined a lazy query that returns the fetchSuggestions function. 

Then I use that function in effect. Before calling, I check if the search query is valid and update the isOpen flag. The fetchSugestions query function takes graphql variables (searchQuery), fetch the data, and returns it to the react component.

graphQL query

The last thing we need to do is create a graphQL query used by the useQuickSearchSuggestions hook.

1export const GET_QUICK_SEARCH_SUGGESTIONS = gql`
2    query getQuickSearchSuggestions($searchQuery: String!) {
3        products(search: $searchQuery) {
4            items {
5                id
6                name
7                small_image {
8                    url
9                }
10                url_key
11                url_suffix
12                price {
13                    regularPrice {
14                        amount {
15                            value
16                            currency
17                        }
18                    }
19                }
20            }
21        }
22    }
23`;
24

Summary

It looks like the Quick Search functionality works good:

So to execute GraphQL queries using Apollo GraphQL client, you can choose between two custom apollo react hooks:

  1. 1. useQuery

  2. 2. useLazyQuery

What is the difference between useQuery hook and useLazyQuery?

When using useLazyQuery, it does not perform the associated query. Instead, the function returns query functions in the result tuples called when the query is executed.

The useLazyQery hook is perfect when you, for example, need to fetch data for the GraphQL server on user action, user clicking, or typing.

Apollo React Hooks and cached data

Apollo client provides a cache mechanism called InMemoryCache. Thanks to that, the apollo cache query results in memory. Of course, Apollo react hooks supports that cache. You can set the default fetch policy globally and locally on each query.

Handling GraphQL errors

Lazy query error handling is pretty the same as when you use the useQuery hook. Please follow this article if you want to get more about GraphQL errors.

Share this article with your friends!

Each share supports and motivates me to create new content. Thank you!

About the author

Marcin Kwiatkowski

Frontend developer, Certified Magento Full Stack Developer, writer, based in Poland. He has eight years of professional Software engineering experience. Privately, husband and father. He likes reading books, drawing, and walking through mountains.

Read more