September 29, 2021

How to Filter and Search using Variables in Apollo Client

Khalil Stemmler

Khalil Stemmler

You know how to query data from Apollo Client using the useQuery hook, but what about searching and filtering it? How does that work?

In this post, we’ll walk through a tiny React and Apollo Client example demonstrating how to set up queries that let you search and filter using variables.

Just want the code?: You can find the code for this blog post @ stemmlerjs/apollo-examples.

Prerequisites

Example: Querying and presenting a list of albums

In this example, we’ll use an API that returns a list of albums. The idea is that we’ll start out with a page that presents an entire list of albums.

When we type in the form and search for an album, the API should return only the albums that match the search.

Let’s get into it.

GraphQL Schema

On the GraphQL API, assume that our schema looks like the following:

type Album {
  id: ID
  name: String
}

input AlbumsInputFilter {
  id: ID
  name: String
}

type Query {
  albums(input: AlbumsInputFilter): [Album]
}

You’ll notice that on the root Query object, we have a query called albums which takes in an optional AlbumsInputFilter input type and returns a list of albums. Not a whole lot going on here.

  • Input types: If you’re unfamiliar with input types, I recommend reading the docs on them
  • Implementing searching & filtering in a GraphQL API: We’re not going to go into detail as to how to implement searching and filtering on the backend in this post. If you’d like to learn how this works, you can read “How to Search and Filter results in GraphQL API“.

Testing the API with Apollo Studio Explorer

If you’re using Apollo Server, you can navigate to the localhost URL where your graph is running and test out the API using Apollo Studio Explorer.

Giving this a quick little test query, all looks well — let’s move on to hooking this up to an Apollo Client + React frontend.

Frontend

After initializing Apollo Client as we normally would in a new React app, to keep things simple, we’ll go ahead and write all of our code in the App.js file.

Here’s where we might start out.

import React from 'react'
import "./App.css";

function App() {
  return (
    <div className="App">
      <h1>Albums</h1>

      <div>
        <label>Search</label>
        <input
          onChange={(e) => { /* Handle updating search/filter text */ }}
          type="string"
        />
      </div>

      <br/>

      {[].map((album) => (
        <div>{JSON.stringify(album)}</div>
      ))}

      <br/>

      <button
        onClick={() => { /* Handle search/filter */ }}
      >
        Submit!
      </button>
    </div>
  );
}

export default App;

The initial query

Next, let’s write the query to make the initial fetch to our GraphQL API to retrieve the list of albums. We do this by:

  • Writing a GraphQL query
  • Utilizing the useQuery hook to fetch the data when the component loads
  • Handling the loading and error states
  • Rendering the resulting data if the query is done loading and there aren’t any errors.

Here’s what the code looks like:

import React from 'react'
import "./App.css";
import { useQuery, gql } from "@apollo/client";
import { useState } from "react";

const GET_ALBUMS = gql`
  query Albums {
    albums {
      id
      name
    }
  }
`;

function App() {
  const { data, loading, error } = useQuery(GET_ALBUMS);

  if (loading) return <div>Loading</div>;
  if (error) return <div>error</div>;

  return (
    <div className="App">
      <h1>Albums</h1>

      <div>
        <label>Search</label>
        <input
          onChange={(e) => { /* Handle updating search/filter text */ }}
          type="string"
        />
      </div>

      <br/>

      {data.albums.map((album) => (
        <div>{JSON.stringify(album)}</div>
      ))}

      <br/>

       <button
        onClick={() => { /* Handle search/filter */ }}
      >
        Submit!
      </button>
    </div>
  );
}

export default App;

We should now see a list of albums returned from the API.

Setting up search state

Now for the fun part. We need to hook up the searching and filtering state.

This means we’ll need to store the current value of the input field as local state. Why? Because when we click “Submit”, we’ll need access to the current value of what was typed in. We will then use that current value as a GraphQL variable in our query.

GraphQL variables?: To better understand GraphQL variables and how they work, I recommend reading “The Anatomy of a GraphQL Query“.

To keep track of the input state, we can use a trivial React hook that exposes the filters local state and a single operation for updating the filters called updateFilter.

import React from 'react'
import "./App.css";
import { useQuery, gql } from "@apollo/client";
import { useState } from "react";

...

function useAlbumFilters() {
  const [filters, _updateFilter] = useState({ 
    id: undefined, 
    name: undefined 
  });

  const updateFilter = (filterType, value) => {
    _updateFilter({
      [filterType]: value,
    });
  };

  return {
    models: { filters },
    operations: { updateFilter },
  };
}

Let’s hook this up to our React component’s input field.

...

function App() {
  const { operations, models } = useAlbumFilters();
  const { data, loading, error, refetch } = useQuery(GET_ALBUMS);

  if (loading) return <div>Loading</div>;
  if (error) return <div>error</div>;

  return (
    <div className="App">
      <h1>Albums</h1>

      <div>
        <label>Search</label>
        <input
          onChange={(e) => operations.updateFilter("name", e.target.value)}
          type="string"
        />
      </div>

      ...
    </div>
  );
}

Great. Now, when we type, the value from the input field is stored as state. The last thing to do is to hook the search value up to the query as a GraphQL variable.

Refetching and filtering

This last step is broken into two parts.

First, we update the query to use a query variable called albumsInput to refer to the input type. It gets passed in to the albums type as an argument.

const GET_ALBUMS = gql`
  query Albums($albumsInput: AlbumsInputFilter) {
    albums(input: $albumsInput) {
      id
      name
    }
  }
`;

In reality, you could name this variable anything you want. For example, someInput would also work. It doesn’t matter what you name it. What does matter is that the input type matches the type used in the schema.

If you’ll recall from the schema, the parameter used in the type signature for the albums query is AlbumsInputFilter. Because GraphQL is strictly typed, we have to ensure we explicitly use the AlbumsInputFilter type for the variable.

type Query {
  albums(input: AlbumsInputFilter): [Album] # Variables are strictly typed
}

We then deconstruct the refetch function from the useQuery hook; and on the onClick callback, we invoke that refetch function with the query variable.


...

function App() {
  const { operations, models } = useAlbumFilters();
  const { data, loading, error, refetch } = useQuery(GET_ALBUMS);

  if (loading) return <div>Loading</div>;
  if (error) return <div>error</div>;

  return (
    <div className="App">
      <h1>Albums</h1>

      ...

      <button
        onClick={() =>
          refetch({
            albumsInput: { name: models.filters.name },
          })
        }
      >
        Submit!
      </button>
    </div>
  );
}

export default App;

Take special note of the structure of the object passed to the refetch function. In the GraphQL query, we write the variable as $albumInput but when we call refetch, we pass in an object with the key albumsInput on the object. This is because refetch‘s function expects an object containing any variables included in the query as keys.

You can read more about “Providing new variables to refetch” in the docs.

Solution

And that’s it — here’s what the complete code looks like:

import React from 'react'
import "./App.css";
import { useQuery, gql } from "@apollo/client";
import { useState } from "react";

const GET_ALBUMS = gql`
  query Albums($albumsInput: AlbumsInputFilter) {
    albums(input: $albumsInput) {
      id
      name
    }
  }
`;

function useAlbumFilters() {
  const [filters, _updateFilter] = useState({ 
    id: undefined, 
    name: undefined 
  });

  const updateFilter = (filterType, value) => {
    _updateFilter({
      [filterType]: value,
    });
  };

  return {
    models: { filters },
    operations: { updateFilter },
  };
}

function App() {
  const { operations, models } = useAlbumFilters();

  const { data, loading, error, refetch } = useQuery(GET_ALBUMS);

  if (loading) return <div>Loading</div>;
  if (error) return <div>error</div>;

  return (
    <div className="App">
      <h1>Albums</h1>

      <div>
        <label>Search</label>
        <input
          onChange={(e) => operations.updateFilter("name", e.target.value)}
          type="string"
        />
      </div>

      <br/>

      {data.albums.map((album) => (
        <div>{JSON.stringify(album)}</div>
      ))}

      <br/>

      <button
        onClick={() =>
          refetch({
            albumsInput: { name: models.filters.name },
          })
        }
      >
        Submit!
      </button>
    </div>
  );
}

export default App;

Conclusion

In this post, we learned how to set up filter and search functionality in Apollo Client.

What’s next?

Now, I’m a believer in “if we do it, we know it”. If you’re just getting started with GraphQL, I highly recommend you check out our completely free “Lift Off” GraphQL course.

In less than 30 minutes, you’ll learn the core components of a full-stack GraphQL app with Apollo through an interactive tutorial.

You can get started here. Enjoy!

Written by

Khalil Stemmler

Khalil Stemmler

Read more by Khalil Stemmler