😇

Hydrogenを試す その2

2022/11/12に公開

以下のドキュメントに沿って、Hydrogenのチュートリアルを実施する。
https://shopify.dev/custom-storefronts/hydrogen/getting-started/tutorial/fetch-data

このチュートリアルでは、Hydrogenアプリをストアフロントに接続し、Storefront API を使用してデータを取得する。
また、このチュートリアルでは以下のことを学習する。

  • Shopify Storefront API GraphQL Explorerを使ってGraphQLクエリを作成する
  • Hydrogenアプリ内でストアフロントのデータをフェッチするuseShopQueryフックを実装する
  • デフォルトのSEOタグを生成する
  • サスペンスを使ってアプリの読み込みシーケンスを改善する

要件

以下のチュートリアルを完了させていること。
https://shopify.dev/custom-storefronts/hydrogen/getting-started/tutorial/begin

Step1. GraphQL Explorerにアクセスする

Hydrogenをローカル開発サーバーで起動している場合、ショップに接続されているインタラクティブなGraphQL Explorerを読み込むことができる。
※ デフォルトではデモストアに接続されている。

GraphQL Explorerには、以下のいずれかのURLからアクセスができる。

GraphQL Explorerにアクセスし、左側のフィールドに以下を入力して実行する。

Query

query ShopInfo {
  shop {
    name
    description
  }
}

Result

{
  "data": {
    "shop": {
      "name": "Hydrogen",
      "description": "A custom storefront powered by Hydrogen, Shopify's React-based framework for building headless."
    }
  }
}

Step2. Hydrogenアプリにクエリを実装する

Step1でクエリのテストを実行し期待通りに動作することを確認したので、クエリをアプリに実装してショップ名を表示できるようにする。

Hydrogenはserverコンポーネント内からストアフロントのデータを取得するためのuseShopQueryフックを提供している。
このステップでは、クエリで取得したショップ名をレンダリングする新しいレイアウトコンポーネントを作成し、レイアウト内でuseShopQueryフックを使用してショップ名を取得するGraphQLクエリを作成する。

まずは、serverコンポーネントとしてlayoutコンポーネントを作成する(Layout.server.tsx)。
(layoutコンポーネントはクライアント側とのインタラクティブ性を必要としないため、serverコンポーネントであるべきである。)

チュートリアルではJavaScriptで書かれているが、TypeScriptで書きたいので一部修正。

import { ReactNode } from 'react';
import { useShopQuery, CacheLong, gql, useUrl, Link } from "@shopify/hydrogen";

/**
 * A server component that defines a structure and organization of a page that can be used in different parts of the Hydrogen app
*/
export function Layout({ children } : { children: ReactNode}) {
  const { pathname } = useUrl();
  const isHome = pathname === "/";

  const {
    data: { shop },
  } = useShopQuery<{ shop : any }>({
    query: SHOP_QUERY,
    cache: CacheLong(),
    preload: true,
  });

  return (
    <>
      <div className="flex flex-col min-h-screen antialiased bg-neutral-50">
        <div className="">
          <a href="#mainContent" className="sr-only">
            Skip to content
          </a>
        </div>
        <header
          role="banner"
          className={`flex items-center h-16 p-6 md:p-8 lg:p-12 sticky backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-4 antialiased transition shadow-sm ${
            isHome ? "bg-black/80 text-white" : "bg-white/80"
          }`}
        >
          <div className="flex gap-12">
            <Link className="font-bold" to="/">
              {shop.name}
            </Link>
          </div>
        </header>

        <main role="main" id="mainContent" className="flex-grow">
          {children}
        </main>
      </div>
    </>
  );
}

const SHOP_QUERY = gql`
  query ShopInfo {
    shop {
      name
      description
    }
  }
`;

作成したLayoutコンポーネントをストアフロントとなるホームページにimportする。

index.server.tsx
import { Layout } from "../components/Layout.server";
export default function Home() {
  return (
    <Layout>
      <div className="p-6 md:p-8 lg:p-12">
        <h1 className="font-extrabold mb-4 text-5xl md:text-7xl">
          Hello world!
        </h1>
        <p className="font-bold mb-3">Welcome to Hydrogen.</p>
        <p>
          Hydrogen is a front-end web development framework used for building
          Shopify custom storefronts.
        </p>
      </div>
    </Layout>
  );
}

上記変更を保存し、http://localhost:3000 にアクセスすると、ページ上部にショップ名が表示され、その下にHello,World!が表示されている画面が見られる。

Step3. SEOタグを生成する

Hydrogen には、Web ページに SEO 情報をレンダリングする Seo クライアントコンポーネントが含まれている。
Seoクライアントコンポーネントは、Storefront APIからのデータを使用して、検索エンジンが探す<head>タグを生成する

検索エンジン用のSEOタグを生成するには、LayoutSeoコンポーネントを追加しショップのタイトルと説明をdatapropに渡す。

Layout.server.tsx
import {
  useShopQuery,
  CacheLong,
  gql,
  useUrl,
  Link,
  Seo,
} from "@shopify/hydrogen";

/**
 * A server component that defines a structure and organization of a page that can be used in different parts of the Hydrogen app
 */
export function Layout({ children }) {
  const { pathname } = useUrl();
  const isHome = pathname === "/";

  const {
    data: { shop },
  } = useShopQuery({
    query: SHOP_QUERY,
    cache: CacheLong(),
    preload: true,
  });

  return (
    <>
      <Seo
        type="defaultSeo"
        data={{
          title: shop.name,
          description: shop.description,
        }}
      />
      <div className="flex flex-col min-h-screen antialiased bg-neutral-50">
        <div className="">
          <a href="#mainContent" className="sr-only">
            Skip to content
          </a>
        </div>
        <header
          role="banner"
          className={`flex items-center h-16 p-6 md:p-8 lg:p-12 sticky backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-4 antialiased transition shadow-sm ${
            isHome ? "bg-black/80 text-white" : "bg-white/80"
          }`}
        >
          <div className="flex gap-12">
            <Link className="font-bold" to="/">
              {shop.name}
            </Link>
          </div>
        </header>

        <main role="main" id="mainContent" className="flex-grow">
          {children}
        </main>
      </div>
    </>
  );
}

const SHOP_QUERY = gql`
  query ShopInfo {
    shop {
      name
      description
    }
  }
`;

デベロッパーツールなどでhtmlを確認すると、<head>タグの中にSEOタグが追加されていることがわかる。

Step4. Collectionを取得する

FeaturedCollectionsコンポーネントを作成する

FeaturedCollectionsコンポーネントでは、StoreFront APIで取得したCollectionをuseShopQueryを介して表示している。

FeaturedCollections.server.tsx
import { Link, Image, gql, useShopQuery, CacheLong } from "@shopify/hydrogen";

export default function FeaturedCollections() {
  const {
    data: { collections },
  } = useShopQuery({
    query: QUERY,
    cache: CacheLong(),
  });

  return (
    <section className="w-full gap-4 md:gap-8 grid p-6 md:p-8 lg:p-12">
      <h2 className="whitespace-pre-wrap max-w-prose font-bold text-lead">
        Collections
      </h2>
      <div className="grid-flow-row grid gap-2 gap-y-6 md:gap-4 lg:gap-6 grid-cols-1 false  sm:grid-cols-3 false false">
        {collections.nodes.map((collection) => {
          return (
            <Link key={collection.id} to={`/collections/${collection.handle}`}>
              <div className="grid gap-4">
                <h2 className="whitespace-pre-wrap max-w-prose font-medium text-copy">
                  {collection.title}
                </h2>
              </div>
            </Link>
          );
        })}
      </div>
    </section>
  );
}

const QUERY = gql`
  query FeaturedCollections {
    collections(first: 3, query: "collection_type:smart", sortKey: UPDATED_AT) {
      nodes {
        id
        title
        handle
      }
    }
  }
`;
index.server.tsx
import FeaturedCollections from "../components/FeaturedCollections.server";
import { Layout } from "../components/Layout.server";
export default function Home() {
  return (
    <Layout>
      <FeaturedCollections />
    </Layout>
  );
}

この段階では、画面は以下のようになる。

Collectionの画像を取得する

FeaturedCollections.server.tsxのGraphQLを修正して、Collectionの画像を取得する。

FeaturedCollections.server.tsx
import { Link, Image, gql, useShopQuery, CacheLong } from "@shopify/hydrogen";

export default function FeaturedCollections() {
  const {
    data: { collections },
  } = useShopQuery({
    query: QUERY,
    cache: CacheLong(),
  });

  return (
    <section className="w-full gap-4 md:gap-8 grid p-6 md:p-8 lg:p-12">
      <h2 className="whitespace-pre-wrap max-w-prose font-bold text-lead">
        Collections
      </h2>
      <div className="grid-flow-row grid gap-2 gap-y-6 md:gap-4 lg:gap-6 grid-cols-1 false  sm:grid-cols-3 false false">
        {collections.nodes.map((collection) => {
          return (
            <Link key={collection.id} to={`/collections/${collection.handle}`}>
              <div className="grid gap-4">
                {collection?.image && (
                  <Image
                    className="rounded shadow-border overflow-clip inline-block aspect-[5/4] md:aspect-[3/2] object-cover"
                    width={"100%"}
                    height={336}
                    alt={`Image of ${collection.title}`}
                    data={collection.image}
                  />
                )}
                <h2 className="whitespace-pre-wrap max-w-prose font-medium text-copy">
                  {collection.title}
                </h2>
              </div>
            </Link>
          );
        })}
      </div>
    </section>
  );
}

const QUERY = gql`
  query FeaturedCollections {
    collections(first: 3, query: "collection_type:smart", sortKey: UPDATED_AT) {
      nodes {
        id
        title
        handle
        image {
          altText
          width
          height
          url
        }
      }
    }
  }
`;

この段階での画面表示は以下のようになる。

Step5. Suspenseでローディングシーケンスを改善する

Suspenseは、非同期データフェッチ中にコンポーネント内のプレースホルダーコンテンツの外観と動作を制御するReactの機能。
React 18では、ストリーミングSSRを補完するためにデータフェッチにSuspenseが導入された。

Suspenseについては以下がわかりやすい。
https://shopify.dev/custom-storefronts/hydrogen/framework/streaming-ssr#example-no-suspense-component-defined

FeaturedCollections は Storefront API からデータを取得するサーバーコンポーネントで、コンポーネントの読み込みが終了するまで、React は最も近い Suspense の境界を探し、指定された読み込みのフォールバックを表示する。
現在、最も近い Suspense の祖先は App.server.tsx でアプリ全体をラップしている。FeaturedCollections を独自の Suspense 境界でラップすることで、ページ上の他のコンポーネントをブロックすることがなくなる。

index.server.tsx
import { Suspense } from "react";

import FeaturedCollections from "../components/FeaturedCollections.server";
import { Layout } from "../components/Layout.server";

export default function Home() {
  return (
    <Layout>
      <Suspense>
        <FeaturedCollections />
      </Suspense>
    </Layout>
  );
}
Layout.server.tsx
import {
  useShopQuery,
  CacheLong,
  gql,
  useUrl,
  Link,
  Seo,
} from "@shopify/hydrogen";
import { Suspense } from "react";

/**
 * A server component that defines a structure and organization of a page that can be used in different parts of the Hydrogen app
 */
export function Layout({ children }) {
  const { pathname } = useUrl();
  const isHome = pathname === "/";

  const {
    data: { shop },
  } = useShopQuery({
    query: SHOP_QUERY,
    cache: CacheLong(),
  });

  return (
    <>
      <Suspense>
        <Seo
          type="defaultSeo"
          data={{
            title: shop.name,
            description: shop.description,
          }}
        />
      </Suspense>
      <div className="flex flex-col min-h-screen antialiased bg-neutral-50">
        <div className="">
          <a href="#mainContent" className="sr-only">
            Skip to content
          </a>
        </div>
        <header
          role="banner"
          className={`flex items-center h-16 p-6 md:p-8 lg:p-12 sticky backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-4 antialiased transition shadow-sm ${
            isHome ? "bg-black/80 text-white" : "bg-white/80"
          }`}
        >
          <div className="flex gap-12">
            <Link className="font-bold" to="/">
              {shop.name}
            </Link>
          </div>
        </header>

        <main role="main" id="mainContent" className="flex-grow">
          <Suspense>{children}</Suspense>
        </main>
      </div>
    </>
  );
}

const SHOP_QUERY = gql`
  query ShopInfo {
    shop {
      name
      description
    }
  }
`;

Discussion