How do I make dynamic API calls (onClick) using Remix & GraphQL?

Hi all,

I’m new to both Remix and GraphQL (although I have experience with React & TypeScript development).

I’m building an app that uses a Prisma database as it’s primary storage. When the page loads, I use the loader Remix function to load data from the database - this works as expected. However, I need to allow the user to import/sync data from the Shopify Admin API into the database when they click a button. I’ve read all of the docs and countless online blogs, and I’ve surmised that:

  • loader is used to make GET requests (or query in GraphQL) when a route/page loads
  • action is used to make POST/PUT requests (or mutation in GraphQL) when an action on the page is taken (i.e. form submit)
  • useFetcher can be used to make requests dynamically, by using fetcher.load to load a route

To get this working, I have done the following:
app.customers.jsx

const fetcher = useFetcher();

const importCustomers = () => {
  if (fetcher.state !== "idle") return;
  fetcher.load("/app/import"); // Triggers data fetching
}

useEffect(() => {
  if (!fetcher.data || fetcher.state !== "idle") {
    return;
  }
}, [fetcher.state, fetcher.data]);

return (
    <Page
      primaryAction={{ content: fetcher.state !== "idle" ? "Generating report..." : "Generate report", onAction: () => importCustomers() }}
    />
)

app.import.js

import { getAdminCustomers } from "../models/customers.server";
import { authenticate } from "../shopify.server";

export async function loader({ request }) {
  const { admin } = await authenticate.admin(request);
  const customers = await getAdminCustomers(admin);
  return Response.json(customers)
}

customers.server.js

export const getAdminCustomers = async (admin) => {
  try {
    const response = await admin.graphql(
      `#graphql
       query getCustomers {
        customerSegmentMembers(first: 1000, query: "customer_tags CONTAINS 'b2b'") {
          edges {
            node {
              id
              displayName
              defaultEmailAddress {
                emailAddress
              }
            }
          }
        }
      }`
    );

    if (response.ok) {
      const {
        data: {
          customerSegmentMembers: { edges }
        }
      } = await response.json();

      return edges;
    }

  } catch (error) {
    return error;
  }
}

The above code works - when the user clicks the ‘Import’ button, the app loads the route /app/import, which runs the loader, calls the API via GraphQL and returns the data. However, surely there must be a better way of making API calls dynamically?

Apologies if I’ve misinterpreted Remix/GraphQL, and that actually they aren’t designed for this type of usecase. Any advice would be appreciated.

Hi,

Instead of using loader , I believe you’ll want to define an action in your app.import.js file to handle the user-triggered import operation. Your file should look something like this:

import { getAdminCustomers } from "../models/customers.server";
import { authenticate } from "../shopify.server";

export async function action({ request }) {
  const { admin } = await authenticate.admin(request);
  const customers = await getAdminCustomers(admin);
  // Process and save customers to your database here
  return new Response(JSON.stringify(customers), {
    headers: { "Content-Type": "application/json" },
  });
}

Thanks for getting back to me @Liam-Shopify. I understand that an action might be better suited, I’ll give that a go. But I still have the question as to whether this is the correct pattern for handling an import, i.e. Triggering a useFetcher hook on a button click in one route, to run a loader or action function in another route? Is there not a less convoluted way of making dynamic calls to the API?

Thanks again!

I’ve connected with a Remix expert internally and he’s advised that you can simplify this a little bit by using a form instead of wiring up a callback:

<fetcher.Form method="get" action="/app/import"> <button type="submit">Get data</button> </fetcher.Form>

Additionally in general resource routes like you’ve setup are a good way to have specific data you want to fetch based on user action.

That’s a really good idea, I’ll give that a go. Really appreciate the help Liam, thank you!

1 Like