Calling API Routes from the client

Learn how to use React Query to call your API route handlers from React components

Makerkit includes the library React Query, a data-fetching React utility library.

This library is useful for making asynchronous requests from your React components. For example:

  1. When interacting with the Supabase Client (DB, Storage, Auth, etc.)
  2. When making requests to API Routes

Why is it useful? React Query makes it simpler to fetch and mutate data, caching and revalidating. Let's see how we can use it to make requests to our endpoints.

Should I use React Query or Remix's useFetcher/useSubmit?

No exact answer - but here is what I do:

  1. For actions that need to trigger a refresh of the UI, I use Remix's useSubmit/useRefresh hook.
  2. For pulling data from the server-side as a result of user interactions, I use React Query.

Ultimately, you should use what you feel more comfortable with as both approaches are valid.

Using React Query for fetching data from an API Route Handler

Let's assume we have a very simple API Route at routes/

import { json } from "@remix-run/node"; export async function loader() { return json({ hello: "world" }); }

As you may know, we can call the endpoint /resources/data using a GET request and will receive the JSON response { "hello": "world" }.

Now, we want to call this from our Remix application using React Query, and use it within a React component.

We proceed building a React Hook with React Query which is able to fetch the data from our endpoint /api/data:

import { useQuery } from '@tanstack/react-query'; export function useFetchData() { const key = '/api/data'; return useQuery([key], async () => { return fetch(key).then(res => res.json()) }); }

Now, we can call this endpoint from any React component:

function MyComponent() { const { data, loading, error } = useFetchData(); if (loading) { return <p>Loading...</p>; } if (error) { return <p>Error...</p>; } return <div>{data.hello}</div> }

Mutating data with React Query

When it comes to mutating data, we can use the useMutation utility, designed to make mutations simple.

As you may know, mutations will likely use the method POST, PUT, PATCH, or DELETE. In these cases, the middleware in the kit will require us to send a CSRF token for security reasons.

As such, we provide a handy wrapper around fetch.

Let's write a very simple API Route that makes a mutation: we add a new record in the tasks table, and return the ID back to the client.

import { ActionFunctionArgs, json } from "@remix-run/node"; export async function action(args: ActionFunctionArgs) { const client = getSupabaseServerClient(args.request); const session = await requireSession(client); const task = args.request.json(); const { data } = await client.from('tasks').insert({ name:, done: false, user_id:, }) .select('id') .single(); return json({ id: }); }

Now, let's write a React Hook that calls this endpoint using React Query.

import { useMutation } from '@tanstack/react-query'; import useFetch from '~/core/hooks/use-fetch'; interface Task { name: string; } function useInsertTask() { const fetcher = useFetch(); const path = '/api/task'; return useMutation( path, async (data: Task) => { return fetcher({ path, body: data, method: 'POST' }); } ); }

What does useFetch do? This hook automatically inserts the CSRF token in our fetch requests, so that you don't have to.

If you prefer not using it, you can manually add the CSRF token instead:

import { useMutation } from '@tanstack/react-query'; import useCsrfToken from '~/core/hooks/use-csrf-token'; interface Task { name: string; } function useInsertTask() { const csrfToken = useCsrfToken(); const path = '/api/task'; return useMutation((data: Task) => { return fetch(path, { body: JSON.stringify(data), method: 'POST', headers: { 'x-csrf-token': csrfToken } }); }); }

We can now call the mutation from a React component:

function TaskForm() { const insertTaskMutation = useInsertTask(); const onSubmit: React.FormEventHandler<HTMLFormElement> = (event) => { const name = new FormData(event.currentTarget).get('name') ?? 'No Name'; const task = { name }; return insertTaskMutation.mutateAsync(task); }; return ( <form onSubmit={onSubmit}> <input type={'name'} required /> <button disabled={insertTaskMutation.loading}> Submit </button> </form> ); }

Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS