Not Just Apollo Client! Why TanStack Query (React Query) is Recommended for GraphQL

2024-10-11

Not Just Apollo Client! Why TanStack Query (React Query) is Recommended for GraphQL

Introduction

While Apollo Client is the first choice when it comes to working with GraphQL in the frontend, TanStack Query offers pretty extensive feature set and easy integration when it comes to working with not just GraphQL but promises in general. In this Post, We’ll look at a basic comparison with GraphQL client and look at a sample implementation in TanStack Query.

What is TanStack Query?

https://github.com/TanStack/query

TanStack Query (formerly React Query) is a library that simplifies data fetching, caching, and updating in React applications. Traditionally, tasks like data fetching, caching, and error handling required manual implementation, but TanStack Query allows you to handle these complex processes simply.

  • Can efficiently fetch data in combination with any function that returns a Promise
  • Automatically caches retrieved data and reuses it to improve performance
  • Automatically updates to the latest information when data changes
  • Supports error handling and retries
  • Allows visual debugging of query states and cache using Devtools

Particularly important is that TanStack Query does not depend on the backend technology stack and can easily integrate with GraphQL, REST API, and other data sources. Since it can handle any function that returns a Promise, it offers high flexibility and is easy to introduce into existing codebases.

This allows for clear separation of server state management and UI logic, improving code readability and maintainability. Using TanStack Query, you can focus on business logic without worrying about complex implementations related to data fetching.

Why use TanStack Query over Apollo Client?

Why TanStack query over Apollo Client? It’s easier to use than Apollo Client.

Working with Apollo Client comes with a main challenge of a steep learning curve with understanding it’s API. The concepts of API fetching, deduplicating requests, state management, caching are independent of the GraphQL.

How TanStack Query differs here that it doesn’t focus on GraphQL but works with anything that supports promises which gives a clear separation of concerns in our codebase. You may switch GraphQL with REST and all the TanStack Query code will more or less the same. This also makes learning and implement features in isolation and in whatever capacity needed. For a more detailed list of feature comparison you can have a look at this:

https://tanstack.com/query/v4/docs/framework/react/comparison

Implementation

Enough talk, where’s the code.

Note that, since TanStack Query doesn’t have an API client of it’s own, we’ll use graphql-request which is an easy to use library with graphql queries.

Once you have a react-template setup (we recommend using vite) we need to install the following packages:

# dependency
npm i graphql tanstack/react-query

# dev dependencies
npm i -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request @graphql-codegen/typescript-operations

# optional 
npm i -D @tanstack/react-query-devtools

In package.json add:

"graphql": {
    "schema": "path/to/your/graphql/backend/schemas/queries/*.graphql"
 }

Let’s configure graphql-codegen .

See also: https://the-guild.dev/graphql/codegen/docs/getting-started

codegen.ts

import { CodegenConfig } from "@graphql-codegen/cli";

// This can be any path in your codebase
const rootGQLDirectory = "src/lib/gql/__gen__/";
const pluginsDirectory = `${rootGQLDirectory}/plugins`;

const config: CodegenConfig = {
  // can be local or a url 
  schema: "path/to/your/graphql/backend/schemas/queries/*.graphql",
  // where we are writing all our graphql queries on the frontend
  documents: ["src/lib/gql/**/queries.graphql"],
  ignoreNoDocuments: true, // for better experience with the watcher
  generates: {
    [rootGQLDirectory]: {
      preset: "client",
    },
    // Integrates graphql-request using codegen. 
    // For more plugins check thes source
    [`${pluginsDirectory}/graphql-request.ts`]: {
      plugins: ["typescript", "typescript-operations", "typescript-graphql-request"],
    },
  },
};

export default config;

And run it:

"scripts": {
		// for generating once
    "gql": "graphql-codegen",
    // watch and update automatically
    "gql:watch": "graphql-codegen --watch",
  },
npm run gql

This will create the following files in the rootGQLDirectory:

├── fragment-masking.ts
├── gql.ts
├── graphql.ts
├── index.ts
└── plugins
    └── graphql-request.ts

We don’t need to know what each of these files contains as we’ll use them directly.

All the prerequisite are complete. For an actual implementation let’s use a sample TODO example.

We need to setup context for both TanStack Query and GraphQL so we can use the corresponding hooks in the underlying components.

GraphQL Request Provider

GraphQLRequestProvider.tsx

If you need to add any additional parameters in request, you can do that here for example adding authorization header:

import { GraphQLClient } from "graphql-request";
import { createContext, ReactNode, useContext } from "react";

import { Sdk, getSdk } from "/path/to/your/gen/plugins/graphql-request";

const GraphQLContext = createContext<Sdk | undefined>(undefined);

export const useGraphQLContext = () => useContext(GraphQLContext);

export const GraphQLRequestProvider = ({ children }: { children?: ReactNode }) => {
  const endpoint = `${YOUR_API_URL}`;
  const graphQLClient = new GraphQLClient(endpoint);
  const sdk = getSdk(graphQLClient);

  return <GraphQLContext.Provider value={sdk}>{children}</GraphQLContext.Provider>
  
};

Tanstack Query Provider

TanStackQueryProvider.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export { useGraphQLContext } from "path/to/your/GraphQLRequestProvider";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 20 seconds
      staleTime: 20 * 1000,
    },
  },
});

const TanStackQueryProvider = ({ children }: { children?: React.ReactNode }) => {
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};

export { TanStackQueryProvider };

Include them both in the App.tsx :

const App: React.FC = () => {
  return (
    <TanStackQueryProvider>
      <GraphQLRequestProvider>{children}</GraphQLRequestProvider>
    </TanStackQueryProvider>
  );
}

The folder structure:

├── index.ts -- exports the necessary components
├── queries.graphql -- our actual graphql query or mutation
├── useApi.ts -- abstracted methods that are actually used
└── useQueries.ts -- Tanstack Query and graphql-request implementation

queries.graphql

fragment Todo on TODO {
	id
  text
  status
}

query GetTodo {
  ...Todo
}

mutation CreateTodo($input: CreateTodoInput!) {
  createTodo(input: $input) {
    todo {
      ...Todo
    }
  }
}

mutation DeleteTodo($input: DeleteTodoInput!) {
  deleteTodo(input: $input) {
    id
  }
}

useQueries.ts

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useGraphQLContext } from "/path/to/your/GraphQLRequestProvider.tsx";

// Keys used in Tanstack Query cache
enum TodoQueryKeys {
  GetTodo = "getTodo",
}

type Todo = {
  id: string,
  text: string,
  status: string
}

export const useQueries = () => {
  const graphQLContext = useGraphQLContext();
  const queryClient = useQueryClient();

  const createTodoMutation = useMutation({
    // The mutation function
    mutationFn: async (name: string) => {
      // Notice the method imported from graphql-request plugin
      // This also provide type intellisense for inputs
      const data = await graphQLContext?.CreateTodo({ input: { text } });
      return data.createTodo.todo;
    },
    onSuccess: newTodo => {
      // After a new todo is created, no need to fetch the todos again. Add them to the existing list
      queryClient.setQueryData([TodoQueryKeys.GetTodo], (data: Todo[]) => [
        ...data,
        newTodo,
      ]);
    },
  });

  const useGetTodosQuery = () =>
    useQuery({
      queryKey: [TodoQueryKeys.GetTodo],
      queryFn: async () => {
        const data = await graphQLContext?.GetTodo();
        return data.getTodo.todo;
      },
      // TODOs can be cached for infinity
      staleTime: Infinity,
    });


  const deleteTodoMutation = useMutation({
    mutationFn: async (todoId: string) => {
      const data = await graphQLContext?.DeleteTodo({ input: { todoId } });
      return data?.deleteTodo.id;
    },
    // After a new todo is deleted, no need to fetch the todos again. Remove them from the existing list
    onSuccess: todoId => {
      queryClient.setQueryData([TodoQueryKeys.GetTodo], (data: Todo[]) => {
        data.splice(
          data.findIndex(w => w.id === todoId),
          1,
        );
        return [...data];
      });
    },
  }); 

  return {
    createTodoMutation,
    useGetTodosQuery,
    deleteTodoMutation   
  };
};

useApi.ts

import { useQueries } from "./useQueries";

export const useTodo = () => {
  const {
    createTodo,
    useGetTodosQuery,
    deleteTodoMutation,
  } = useQueries();

  const createTodo = async (name: string): Promise<NewTodoMutation> => {
    const { mutateAsync, ...rest } = createTodo;
    try {
      const data = await mutateAsync(name);
      return { todo: data, ...rest };
    } catch (_err) {
      return { todo: undefined, ...rest };
    }
  };

  const useGetTodo = (): GetTodos => {
    const { data: todos, ...rest } = useGetTodosQuery();
    return {
      todos,
      ...rest,
    };
  };

  const deleteTodo = async (todoId: string): Promise<DeleteTodoMutation> => {
    const { mutateAsync, ...rest } = deleteTodoMutation;
    try {
      const data = await mutateAsync(todoId);
      return { todo: data, ...rest };
    } catch (_err) {
      return { todo: undefined, ...rest };
    }
  };

  return {
    createTodo,
    useGetTodo,
    deleteTodo
  };
};

Using the API (Todo.tsx ):

import React from 'react';
import { useTodo } from '/path/to/your/useApi.ts'; // Assuming these functions are imported from an API file

const TodoList = () => {
  const { createTodo, useGetTodo, deleteTodo } = useTodo();

  const { todos, isLoading } = useGetTodo();

  const handleCreateTodo = async () => {
    const newTodo = {
      title: 'New Todo',
      status: false
    };
    await createTodo(newTodo);
    // Todos will be automatically updated from Tanstack Query
  };

  const handleDeleteTodo = async (id) => {
    await deleteTodo(id);
    // Todos will be automatically updated from Tanstack Query
  };

  return (
    <>
      {isLoading ? (
        <div>
          <button onClick={handleCreateTodo}>Create Todo</button>
          <ul>
            {todos.map(todo => (
              <li key={todo.id}>
                {todo.title}
                <button onClick={() => handleDeleteTodo(todo.id)}>Delete</button>
              </li>
            ))}
          </ul>
        </div>
      ) : (
        <div> Loading...</div>
      )}
    </>
  );
};

export default TodoList;

Conclusion

We defined a structure for the implementation providing enough abstraction that the underlying API client and state management can be swapped with other components with minimal changes in UI layer.

As you can it’s pretty easy to get started with GraphQL using TanStack Query and graphql-request. This is pretty basic implementation and there are lot more features where each of them will require it’s own blog. The best place to check for more details in the TanStack Query documentation.

Happy Hacking!

English

Eukaryaでは様々な職種で積極的にエンジニア採用を行っています!OSSにコントリビュートしていただける皆様からの応募をお待ちしております!

Eukarya 採用ページ

Eukarya is hiring for various positions! We are looking forward to your application from everyone who can contribute to OSS!

Eukarya Careers

Eukaryaは、Re:Earthと呼ばれるWebGISのSaaSの開発運営・研究開発を行っています。Web上で3Dを含むGIS(地図アプリの公開、データ管理、データ変換等)に関するあらゆる業務を完結できることを目指しています。ソースコードはほとんどOSSとしてGitHubで公開されています。

Eukarya Webサイト / ➔ note / ➔ GitHub

Eukarya is developing and operating a WebGIS SaaS called Re:Earth. We aim to complete all GIS-related tasks including 3D (such as publishing map applications, data management, and data conversion) on the web. Most of the source code is published on GitHub as OSS.

Eukarya Official Page / ➔ Medium / ➔ GitHub