Event-driven API calls in Hydrogen

Via the useFetcher hook

I have been assigned the task of transferring my employer's Shopify Storefront into a Hydrogen headless build for the past few weeks now.

Part of our store includes a signup form, where users can sign up for email discounts before checkout. They enter their email and password, select enter, then the info is to be sent to the Storefront API.

This had been giving me headaches for a while. Most of the Shopify Hydrogen documentation focuses on making API calls on component render, and not on event-driven scenarios. However, I finally found a solution and hopefully my solution can help.

Solution

In Hydrogen, you can not simply make API calls on the client side, like you can in other frameworks. This is to ensure security, so no one can access private storefront information from the client-side. To safely send data to a server component upon an event (form submission in my case), we will be using the useFetcher hook from Remix.

First, Make 2 Components

Make sure both of these components can be routed to. I am making both of them hybrid components, but you can make them client .client.jsx and server .server.jsx respectfully.

We need ...

  • A component that contains an event (I will be using form submission)

  • A component that calls the API via an action function

// The event triggering component 
// Collects a form for user's email and password

import { useState, useEffect } from 'react';
import { useFetcher } from '@remix-run/react';

// empty loader function
export async function loader({context}) {
  return null; 
}

export default function CustomerSignUp() {

  const [customerInput, setCustomerInput] = useState({
    acceptsMarketing: true,
    email: "",
    password: "",
  });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setCustomerInput(input => ({
      ...input,
      [name]: value
    }));
  };

  const handleSignup = async (e) => {
    e.preventDefault();

    // where we will call our server component
  };



  return (
    <div>
      <form>
        <div>Subscription Signup</div>

        <input
          type="email"
          name="email"
          value={customerInput.email}
          onChange={handleInputChange}
          placeholder="Email"
          required
        />

        <input
          type="password"
          name="password"
          value={customerInput.password}
          onChange={handleInputChange}
          placeholder="Password"
          required
        />

        <button type="submit" className=''>Sign Up</button>
      </form>
    </div>
  );
}
// Server Side Component 
// Will call the Storefront API securely 

import { useLoaderData } from '@remix-run/react';

// empty loader function
export async function loader({ request }) {
    return null.
};

// action method is required for receiving POST requests 
export const action = async ({ request }) => {

    // the query I need to send to the Storefront API        
    const mutation = `
      mutation CreateCustomer($input: CustomerCreateInput!) {
        customerCreate(input: $input) {
          customer {
            id
            email
            firstName
            lastName
            acceptsMarketing
            phone
          }
          customerUserErrors {
            code
            field
            message
          }
        }
      }
    `;
};


// This is not neccessary
export default function SignUp() {
  return (
    <></>
  );
}

Implement useFetcher Hook from Remix

The useFetcher hook will be used in the client component. It allows us to make requests to our server component. In this case, I will be using it for a POST request.

// Final Event Component 
// Implements useFetcher hook
import { useState, useEffect } from 'react';

// import useFetcher
// ============================================
import { useFetcher } from '@remix-run/react';
// ============================================

export async function loader({context}) {
  return null; 
}

export default function CustomerSignUp() {

  // How you initialize a useFetcher instance
  // ============================================
  const fetcher = useFetcher();
  // ============================================

  const [customerInput, setCustomerInput] = useState({
    acceptsMarketing: true,
    email: "",
    password: "",
  });

  // console logs fetcher data when updated 
  // ============================================
  useEffect(() => {
    if (fetcher.data) {
      console.log(fetcher.data);
    }
  }, [fetcher.data]);
  // ============================================



  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setCustomerInput(input => ({
      ...input,
      [name]: value
    }));
  };



  const handleSignup = async (e) => {
    e.preventDefault();

    // How to submit a POST request to another component
    // using fetcher
    // ============================================
    fetcher.submit({
      email: customerInput.email,
      password: customerInput.password,
    }, {
      method: "post",
      action: "/signup", // Route to your server component
    });
    // ============================================

 };



  return (
    <div>
      <form 
        onSubmit={handleSignup} 
      >
        <div>Subscription Signup</div>

        <input
          type="email"
          name="email"
          value={customerInput.email}
          onChange={handleInputChange}
          placeholder="Email"
          required
        />

        <input
          type="password"
          name="password"
          value={customerInput.password}
          onChange={handleInputChange}
          placeholder="Password"
          required
        />

        <button type="submit" className=''>Sign Up</button>
      </form>
    </div>
  );
}

Now that we are sending data to the server component, we must unpack it and send it to the API. Then, we can handle the API response and return it to the client event component. This is what our server component will look like now.

/**
 * Signup server component
 * Handles API call to storefront API
 */

import { useLoaderData } from '@remix-run/react';


// empty loader function
export async function loader({ request }) {
    return null;
};


export const action = async ({ request }) => {

    // Unpack data from POST Request
    // Then make variables for GraphQL query
    // ============================================
    const formData = await request.formData();
    const email = formData.get("email");
    const password = formData.get("password");

    const variables = {
        input: {
          email,
          password,
        },
      };
    // ============================================


    const mutation = `
      mutation CreateCustomer($input: CustomerCreateInput!) {
        customerCreate(input: $input) {
          customer {
            id
            email
            firstName
            lastName
            acceptsMarketing
            phone
          }
          customerUserErrors {
            code
            field
            message
          }
        }
      }
    `;


    // Making Storefront API Call
    // ============================================
    try {
        const response = await fetch("Storefront API Link", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-Shopify-Storefront-Access-Token": "your-token",
            },
            body: JSON.stringify({ query: mutation, variables }),
        });

        if (!response.ok) {
            throw new Error(`Error: ${response.status}`);
        }

        const responseData = await response.json();

        if (responseData.data && responseData.data.customerCreate) {

            // succesfull customer creation
            return new Response(JSON.stringify({ 
                message: "Customer created successfully", 
                customer: responseData.data.customerCreate.customer
            }), {
                status: 200,
                headers: {
                    "Content-Type": "application/json",
                },
            });

        // handling errors
        } else if (responseData.errors) {
            return new Response(JSON.stringify({ errors: responseData.errors }), {
                status: 400,
                headers: {
                    "Content-Type": "application/json",
                },
            });
        }

    // handling errors 
    } catch (error) {
        return new Response(JSON.stringify({ error: error.message }), {
            status: 500,
            headers: {
                "Content-Type": "application/json",
            },
        });
    }
    // ============================================
};


export default function SignUp() {
  return (
      <></>
  );
}

Summary

The useFetcher hook can be used to send data back and forth between components dynamically. It can be configured (like I have done) to make API calls conditionally and on events. It is very useful, and saved me a ton of headache.

Thanks for reading!