<Back to Updates

Fetching data with Hydrogen in Remix

November 1, 2022

Working with the Storefront API is the fundamental step in building an ecommerce app with Hydrogen. Leveraging Remix’s `context` object, Hydrogen injects a Storefront API client that makes it simple, clear, and quick to get straight to your queries and mutations.

Let’s break it down step by step.

Creating & Injecting the Storefront API Client

Before we can make a query, Hydrogen’s CLI first creates a Storefront Client in Remix’s server entry file. We use Remix’s context API, calling createstorefrontclient inside getLoadContext. We’ve kept all of the configuration exposed so you can change the logic to suit your needs.

Here’s a simple example of what this looks like.

Initialization (/server.js)

import {createStorefrontClient} from '@shopify/hydrogen';
import {createRequestHandler, getBuyerIp} from '@shopify/remix-oxygen';
  
export default {
  async fetch(request, env, executionContext) {
    const cache = await caches.open('hydrogen');

    const handleRequest = createRequestHandler({
      build: remixBuild,
      mode: "production",
      getLoadContext() {
        const {storefront} = createStorefrontClient({
          cache,
          waitUntil: (p) => executionContext.waitUntil(p),
          buyerIp: getBuyerIp(request),
          publicStorefrontToken: env.SHOPIFY_STOREFRONT_API_PUBLIC_TOKEN,
          storefrontApiVersion: env.SHOPIFY_STOREFRONT_API_VERSION,
          storeDomain: env.SHOPIFY_STORE_DOMAIN,
        });

        return {storefront};
      },
    });

    return handleRequest(request);
  },
};

Hydrogen’s createStorefrontClient function accepts a single argument as an object with the following parameters:

createStorefrontClient (/server.js)


{
  /* A Cache instance to power Hydrogen's caching functionality. */
  cache?: Cache;
  /* The current IP address of the buyer. */
  buyerIp?: string;
  /** 
   * Optional ID to group all sub-requests to the 
   * Storefront API together. 
   **/
  requestGroupId?: string;
  /**
   * A function provided for some runtimes like Oxygen to keep the 
   * request alive while asynchronous work like Caching is performed. 
   **/
  waitUntil?: ExecutionContext['waitUntil'];
  
  /* The language and country code the current request. */
  i18n?: {
    language: LanguageCode;
    country: CountryCode;
  };
  /* The host name of the domain (eg: `{shop}.myshopify.com`). */
  storeDomain: string;
  /** 
   *  The Storefront API delegate access token. Refer to 
   *  the authentication (https://shopify.dev/api/storefront#authentication) 
   *  and delegate access token (https://shopify.dev/apps/auth/oauth/delegate-access-tokens) 
   *  documentation for more details. 
   **/
  privateStorefrontToken?: string;
  /** 
   *  The Storefront API access token. Refer to the 
   *  authentication (https://shopify.dev/api/storefront#authentication) 
   *  documentation for more details. 
   **/
  publicStorefrontToken?: string;
  /** 
   *  The Storefront API version. This should usually be the same as 
   *  the version Hydrogen was built for. Learn more about Shopify 
   *  [API versioning](https://shopify.dev/api/usage/versioning) for more details.  
   **/
  storefrontApiVersion: string;
  /**
   *  Customizes which `"content-type"` header is added when using 
   *  `getPrivateTokenHeaders()` and `getPublicTokenHeaders()`. When 
   *  fetching with a `JSON.stringify()`-ed `body`, use `"json"`. When 
   *  fetching with a `body` that is a plain string, use `"graphql"`. 
   *  Defaults to `"json"`
   * 
   *  Can also be customized on a call-by-call basis by passing in `'contentType'` 
   *  to both `getPrivateTokenHeaders({...})` and `getPublicTokenHeaders({...})`, 
   *  for example: `getPublicTokenHeaders({contentType: 'graphql'})`
   */
  contentType?: 'json' | 'graphql';
};

Now with that setup, we’re ready to start querying data from the Storefront API on our routes.

Querying Data

To load data into your Hydrogen app, use a Remix loader and write a GraphQL query. Hydrogen provides a special storefront utility to make queries against your Shopify storefront.

Querying Example (/routes/product/$productHandle.jsx)


import {json, useLoaderData, type LoaderArgs} from '@shopify/remix-oxygen';

/* Export Remix’s loader function, and deconstruct `storefront` from context. */
export async function loader({params: {handle}, context: {storefront}}: LoaderArgs) {

  /* The `storefront` object now lets you query against the Storefront API 
   * using the client we set up in our `server` file earlier. */

  const productQuery = storefront.query(
    `#graphql
      query Product($handle: String!, $country: CountryCode, $language: LanguageCode) {
        @inContext (country: $country, language: $language)
        product(handle: $handle) {
          id
          title
        }
      }
    `,
    {
      /**
       * Pass variables related to the query.
       */
      variables: {
        handle,
        /**
	     *	These variables are automatically added by default, 
	     *	but you can manually set them to override them as well.
         *	country: storefront.i18n.country,
         * 	language: storefront.i18n.language,
         */
      },
      /**
       *  Optionally, override your default API version
       *  by declaring it here. Useful when there is a new 
       *  feature you want to use, but you're not ready to 
       *  update your API everywhere.
       */
        storefrontApiVersion: '2023-01',
      /**
       *  Cache your server-side query with a built-in best practice
       *  default (SWR). Options include: CacheShort (Default), 
       *  CacheLong, CacheCustom, and CacheNone.
       */
      cache: storefront.CacheShort(),
    },
  );

  return json({
    product: await productQuery,
  });
}

export default function Product() {
  const {product} = useLoaderData();

  // ...
}

Sometimes, you will want to prioritize critical data, like product information, while deferring recommendations or reviews to stream in later. For that, we’ll use Remix’s upcoming defer utility (Learn more about defer here).

Defer Example (/routes/product/$productHandle.jsx)


import {defer, useLoaderData} from '@shopify/remix-oxygen';

export async function loader({params: {handle}, context: {storefront}}) {
  const productQuery = storefront.query(
    `#graphql
      query Product($handle: String!) {
        product(handle: $handle) {
          id
          title
        }
      }
    `,
    {
      variables: {
        handle,
      },
    },
  );

  const productRecommendationsQuery = storefront.query(
    `#graphql
      query ProductRecommendations($handle: String!) {
        productRecommendations(handle: $handle) {
          id
          title
        }
      }
    `,
    {
      variables: {
        handle,
      },
    },
  );

  return defer({
    product,
    productRecommendations: productRecommendationsQuery().then(data => data.productRecommendations),
  });
}

export default PageComponent() {
  const {product, recommendations} = useLoaderData();

  return (
    <>
      

Caching Data

A lot of your queries will be for data that isn’t updated often. Hydrogen makes it easy to speed those queries up by caching them with built-in defaults. Hydrogen supports caching at the sub-request level, meaning queries like the one above can have different caching strategies set for each query; to override the default, storefront.CacheShort().

Caching Example (/routes/product/$productHandle.jsx)


export async function loader({params: {handle}, context: {storefront}}) {
    const {product} = storefront.query(
      `#graphql
        query Product($handle: String!) {
          product(handle: $handle) {
            id
            title
          }
        }
      `,
      {
        variables: {
          handle,
        },
        cache: storefront.CacheLong(),
      },
    );
  
    const productRecommendationsQuery = storefront.query(
      `#graphql
        query ProductRecommendations($handle: String!) {
          productRecommendations(handle: $handle) {
              id
              title
          }
        }
      `,
      {
        variables: {
          handle,
        }
      },
    );
  
    return defer({
      product,
      productRecommendations: productRecommendationsQuery().then(data => data.productRecommendations),
    });
  }

Sometimes (like when when data can be mutated by the user in the same page) you may want to disable caching manually by passing storefront.CacheNone(). This will prevent serving stale data when Remix refreshes the loaders after a mutation.

Mutating Data

To mutate data in Remix actions, use the storefront.mutate function. This is just like the query property, except caching is disabled:

Mutation Example (/routes/cart.jsx)



export async function action({request, context: {storefront}}) {
    const formData = await request.formData();
  
    const cartMutation = storefront.mutate(
      `#graphql
        mutation lineItemUpdate($lineId: ID!, $input: CartLineUpdateInput!) {
            lineItemUpdate(lineId: $lineId, input: $input) {
              quantity
            }
        }
      `,
      {
        /**
         * Pass variables related to the query.
         */
        variables: {
          lineId: formData.get('lineId'),
          input: formData.get('input'),
        },
        /**
         * Mutations are NEVER cached by default.
         */
      },
    );
  
    return json({
      status: 'ok',
    });
  }

Injecting country and language directives into queries

The Storefront API accepts an @inContext directive (learn more) to support international pricing, and multiple languages. Whereas you can pass variables directly when calling storefront.query, it’s also possible to inject the language and country variables automatically.

This setup comes with a convenience helper — anywhere when making a query with storefront.query or storefront.mutate, we will auto-inject country and language into the query variables if we detect these two conditions:

  • There is a $country or $language variable detected in the query string
  • No country or language variable is explicitly passed to the query

This means you can define that there is inContext directive in the query statement and not worry about needing to pass in the variables.

For example:

i18n Loader Example (/routes/index.jsx)


const {shop, hero} = await storefront.query(HOMEPAGE_SEO_QUERY, {
  variables: {
    handle: 'freestyle',
  },
});
const HOMEPAGE_SEO_QUERY = `#graphql
  ${COLLECTION_CONTENT_FRAGMENT}
  query collectionContent(
    $handle: String,
    $country: CountryCode,
    $language: LanguageCode
  )
  @inContext(country: $country, language: $language) {
    hero: collection(handle: $handle) {
      ...CollectionContent
    }
    shop {
      name
      description
    }
  }`;

If you want to manually control country or language, simply declaring them as variables will prevent the auto-injection.

i18n Loader Example: Manual Declaration (/routes/index.jsx)


const {shop, hero} = await storefront.query(HOMEPAGE_SEO_QUERY, {
  variables: {
    handle: 'freestyle',
    country: 'US',     // Always query back in US currency
    language: 'EN',    // Always query back in EN language
  },
});
/* ... */

Putting it all together

Let’s put together the key concepts we’ve covered above, and look at how things compare with how we used to approach things.

Querying: The Old Way


import {
  gql,
  useLocalization,
  useRouteParams,
  useShopQuery,
  CacheLong
} from '@shopify/hydrogen';

import {NotFound} from '~/components/index.server';

export default function Product() {
  const {handle} = useRouteParams();
  const {
    language: {isoCode: languageCode},
    country: {isoCode: countryCode},
  } = useLocalization();
  const {
    data: {product},
  } = useShopQuery({
    query: `#graphql
    query Product(
      $country: CountryCode
      $language: LanguageCode
      $handle: String!
    ) @inContext(country: $country, language: $language) {
      product(handle: $handle) {
        id
        title
      }
    }
  `,
    variables: {
      country: countryCode,
      language: languageCode,
      handle,
    },
  });
  const {
    data: {productRecommendations},
  } = useShopQuery({
    query: `#graphql
    query ProductRecommendations(
      $country: CountryCode,
      $language: LanguageCode,
      $handle: String!
    ) @inContext(country: $country, language: $language) 
      productRecommendations(handle: $handle) {
        id
        title
      }
    }`,
    variables: {
      country: countryCode,
      language: languageCode,
      handle,
    },
    cache: CacheLong(),
  });

  if (!product) {
    return 

Querying: The New Way


import {defer} from "@shopify/remix-oxygen";

export async function loader({ 
  params: {handle}, 
  context: {storefront} 
}) {
  const {product} = storefront.query({
    query: `#graphql
    query Product(
      $country: CountryCode,
      $language: LanguageCode,
      $handle: String!
    ) @inContext(country: $country, language: $language) 
      product(handle: $handle) {
        id
        title
      }`,
    variables: {handle},
    cache: storefront.CacheLong()
  });
  const {productRecommendations} = storefront.query({
    query: `#graphql
      query ProductRecommendations(
        $country: CountryCode,
        $language: LanguageCode,
        $handle: String!
      ) @inContext(country: $country, language: $language) 
        productRecommendations(handle: $handle) {
          id
          title
        }
      }`,
    variables: {handle}
  });
  if (!product) {
    throw new Response('Not Found', {
      status: 404,
    });
  }
  return defer({
    product: await product,
    productRecommendations,
  });
}

With loader functions and Remix, you’ll notice that we’re not only writing less code, but the code we do write is much easier to follow.

Get building

Spin up a new Hydrogen app in minutes.

See documentation