Hydrogenを試す その2
以下のドキュメントに沿って、Hydrogenのチュートリアルを実施する。
このチュートリアルでは、Hydrogenアプリをストアフロントに接続し、Storefront API を使用してデータを取得する。
また、このチュートリアルでは以下のことを学習する。
- Shopify Storefront API GraphQL Explorerを使ってGraphQLクエリを作成する
- Hydrogenアプリ内でストアフロントのデータをフェッチする
useShopQuery
フックを実装する - デフォルトのSEOタグを生成する
- サスペンスを使ってアプリの読み込みシーケンスを改善する
要件
以下のチュートリアルを完了させていること。
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する。
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タグを生成するには、Layout
にSeo
コンポーネントを追加しショップのタイトルと説明をdata
propに渡す。
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を介して表示している。
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
}
}
}
`;
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の画像を取得する。
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
については以下がわかりやすい。
FeaturedCollections
は Storefront API からデータを取得するサーバーコンポーネントで、コンポーネントの読み込みが終了するまで、React は最も近い Suspense
の境界を探し、指定された読み込みのフォールバックを表示する。
現在、最も近い Suspense
の祖先は App.server.tsx
でアプリ全体をラップしている。FeaturedCollections
を独自の Suspense
境界でラップすることで、ページ上の他のコンポーネントをブロックすることがなくなる。
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>
);
}
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