faststore-state-management について
faststore-state-managementは、FastStoreのインタラクティブなEC機能を構築するためのスキルです。カート、セッション、検索の状態管理に@faststore/sdkを使い、プラットフォームとの同期、カート検証、URL連動の検索状態を重視します。
主な機能
- 01useCartによるカート操作の管理
- 02useSessionによるセッションデータ管理
- 03useSearchによる検索状態の管理
- 04チェックアウト前のカート検証処理
- 05SDKによるEC分析イベントの送信
活用シーン
- 01カート商品の追加、削除、更新
- 02通貨、ロケール、販売チャネルの変更
- 03フィルター、並べ替え、ページネーションの実装
- 04カート検証中のチェックアウト防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vtex/ skills --skill faststore-state-managementインストール後、Claude Code で/faststore-state-managementを入力して読み込まれたか確認します。
詳細
FastStore SDK の状態管理
このスキルを使用する場面
次の場合にこのスキルを使用します。
- ショッピングカート、ユーザーセッション、商品検索/絞り込み、分析トラッキングを含むインタラクティブな e コマース機能を構築する場合。
- カートの商品を追加、削除、更新する必要がある場合。
- セッションデータ(通貨、ロケール、販売チャネル、郵便番号)を読み取る、または変更する必要がある場合。
- ファセット検索の状態(並び順、選択したファセット、ページネーション)を管理する必要がある場合。
@faststore/sdkのフック(useCart、useSession、useSearch)を使用する場合。
次の場合はこのスキルを使用しないでください。
- 見た目だけを変更する場合 —
faststore-themingスキルを使用してください。 - ネイティブコンポーネントを置き換える、またはカスタマイズする場合 —
faststore-overridesスキルを使用してください。 - GraphQL スキーマを拡張する、またはカスタムデータを取得する場合 —
faststore-data-fetchingスキルを使用してください。
判断ルール
- すべてのカート操作には
useCart()を使用してください。プラットフォームでの検証、価格の確認、分析を自動的に処理します。 - すべてのセッションデータには
useSession()を使用してください。プラットフォームとの同期を保つvalidateSessionミューテーションが実行されます。 - すべての検索状態には
SearchProvider内でuseSearch()を使用してください。URL パラメーターと同期し、ブラウザーの戻るボタンによるナビゲーションに対応します。 - カート、セッション、検索に対して、カスタムの状態管理(React Context、Redux、Zustand、
useState/useReducer)を構築しないでください。SDK のフックは FastStore プラットフォーム統合レイヤーに接続されています。 useCart()のisValidatingを必ず確認し、検証中はチェックアウトへの移動をブロックしてください。- GA4 互換の e コマースイベントトラッキングには、SDK の
sendAnalyticsEvent()を使用してください。
厳守事項
制約:カート、セッション、検索の状態には @faststore/sdk を使用する
カート、セッション、検索の状態管理には、@faststore/sdk のフック(useCart、useSession、useSearch)を必ず使用してください。これらの領域に対して、カスタムの状態管理(React Context、Redux、Zustand、useState/useReducer)を構築してはいけません。
重要な理由
SDK のフックは FastStore プラットフォーム統合レイヤーに接続されています。useCart() はカート検証ミューテーションを実行します。useSession() はロケール/通貨の変更をすべてのデータクエリに反映します。useSearch() は URL パラメーターと同期し、検索の再取得を実行します。カスタム状態管理ではこれらすべてが迂回され、カートは検証されず、価格が古くなる可能性があり、検索は URL と同期されず、分析イベントも発火しません。
検出方法
useState または useReducer でカート商品、カート合計、セッションのロケール、セッションの通貨、検索ファセットを管理している箇所があれば → 中止してください。これらには @faststore/sdk の useCart()、useSession()、useSearch() を使用する必要があります。CartContext、SessionContext、SearchContext のような名前で createContext を使用している箇所があれば → 中止してください。SDK にはこれらのコンテキストがすでに用意されています。
正しい例
// src/components/MiniCart.tsx
import React from 'react'
import { useCart } from '@faststore/sdk'
export default function MiniCart() {
const { items, totalItems, isValidating, removeItem } = useCart()
if (totalItems === 0) {
return <p>Your cart is empty</p>
}
return (
<div data-fs-mini-cart>
<h3>Cart ({totalItems} items)</h3>
{isValidating && <span>Updating cart...</span>}
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.itemOffered.name}</span>
<span>${item.price}</span>
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
</div>
)
}
誤った例
// WRONG: Building a custom cart context instead of using @faststore/sdk
import React, { createContext, useContext, useReducer } from 'react'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
// This custom context duplicates what @faststore/sdk already provides.
// Cart changes here will NOT trigger platform validation.
// Prices and availability will NOT be verified against VTEX.
// Analytics events will NOT fire for add-to-cart actions.
const CartContext = createContext<{
items: CartItem[]
dispatch: React.Dispatch<any>
}>({ items: [], dispatch: () => {} })
function cartReducer(state: CartItem[], action: any) {
switch (action.type) {
case 'ADD':
return [...state, action.payload]
case 'REMOVE':
return state.filter((item) => item.id !== action.payload)
default:
return state
}
}
export function CartProvider({ children }: { children: React.ReactNode }) {
const [items, dispatch] = useReducer(cartReducer, [])
return (
<CartContext.Provider value={{ items, dispatch }}>
{children}
</CartContext.Provider>
)
}
制約:カートの検証状態を必ず処理する
useCart() の isValidating フラグを必ず確認し、カートの検証中は適切なローディング状態を表示してください。isValidating が true の間は、チェックアウトへの移動を許可してはいけません。
重要な理由
カートの検証は非同期処理であり、VTEX プラットフォームに対して、商品の現在の価格、在庫状況、適用可能なプロモーションを確認します。検証中にユーザーがチェックアウトへ進むと、古い価格が表示されたり、エラーが発生したりする可能性があります。isValidating フラグは、これを防ぐためにあります。
検出方法
チェックアウトリンクまたは「チェックアウトに進む」ボタンがあるコンポーネントで、useCart() の分割代入に isValidating が含まれていなければ → 検証状態が処理されていないことを警告してください。移動前に isValidating を確認しないチェックアウトリンクまたはボタンがあれば → カートデータが古くなる可能性について警告してください。
正しい例
// src/components/CartSummary.tsx
import React from 'react'
import { useCart } from '@faststore/sdk'
export default function CartSummary() {
const { items, totalItems, isValidating } = useCart()
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
return (
<div data-fs-cart-summary>
<p>{totalItems} item{totalItems !== 1 ? 's' : ''} in your cart</p>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
{isValidating && (
<p data-fs-cart-validating>Verifying prices and availability...</p>
)}
<a
href="/checkout"
data-fs-checkout-button
aria-disabled={isValidating}
onClick={(e) => {
if (isValidating) {
e.preventDefault()
}
}}
>
{isValidating ? 'Updating cart...' : 'Proceed to Checkout'}
</a>
</div>
)
}
誤った例
// WRONG: Ignoring cart validation state
import React from 'react'
import { useCart } from '@faststore/sdk'
export default function CartSummary() {
const { items, totalItems } = useCart()
// Missing isValidating — user can click checkout while cart is being validated.
// This can lead to price mismatches at checkout or failed orders.
return (
<div>
<p>{totalItems} items</p>
<a href="/checkout">Proceed to Checkout</a>
{/* No loading state. No validation check. User may proceed with stale prices. */}
</div>
)
}
制約:セッションデータをlocalStorageに保存しないでください
セッションデータ(通貨、ロケール、チャネル、人物)にアクセスするには、必ず@faststore/sdkのuseSession()を使用してください。セッションデータをlocalStorageまたはsessionStorageから直接読み書きしてはいけません。
重要な理由
SDKのセッションモジュールは、VTEXプラットフォームとの同期を管理します。セッションデータが変更されると、SDKはvalidateSessionミューテーションを実行し、サーバー側のセッションを更新してカートを再検証します。localStorageに直接書き込むと、この検証が行われません。そのため、プラットフォームは変更を認識できず、価格が誤った通貨で表示されたり、カート内の商品に正しい販売チャネルが反映されなかったりする可能性があります。
検出
セッションデータに関連するキー(currency、locale、channel、region、postalCode)を使ったlocalStorage.getItemまたはlocalStorage.setItemが見つかった場合は、作業を中断してください。これらのデータはuseSession()で管理する必要があります。同じ目的でsessionStorageが使われている場合も、作業を中断してください。
正しい例
// src/components/LocaleSwitcher.tsx
import React from 'react'
import { useSession } from '@faststore/sdk'
export default function LocaleSwitcher() {
const { locale, currency, setSession } = useSession()
const handleLocaleChange = (newLocale: string, newCurrency: string) => {
// setSession triggers platform validation and re-fetches data
setSession({
locale: newLocale,
currency: { code: newCurrency, symbol: newCurrency === 'USD' ? '$' : 'R$' },
})
}
return (
<div data-fs-locale-switcher>
<button
onClick={() => handleLocaleChange('en-US', 'USD')}
aria-pressed={locale === 'en-US'}
>
EN
</button>
<button
onClick={() => handleLocaleChange('pt-BR', 'BRL')}
aria-pressed={locale === 'pt-BR'}
>
PT
</button>
<span>Current: {locale} ({currency.code})</span>
</div>
)
}
誤った例
// WRONG: Managing session data manually via localStorage
import React, { useState, useEffect } from 'react'
export default function LocaleSwitcher() {
const [locale, setLocale] = useState('en-US')
useEffect(() => {
// WRONG: Reading session data from localStorage
const saved = localStorage.getItem('store-locale')
if (saved) setLocale(saved)
}, [])
const handleLocaleChange = (newLocale: string) => {
// WRONG: Writing session data to localStorage
// The VTEX platform does NOT know about this change.
// Product prices, availability, and cart will NOT update.
localStorage.setItem('store-locale', newLocale)
setLocale(newLocale)
}
return (
<div>
<button onClick={() => handleLocaleChange('en-US')}>EN</button>
<button onClick={() => handleLocaleChange('pt-BR')}>PT</button>
</div>
)
}
推奨パターン
SDKフックを組み合わせた推奨される使い方:
// src/components/CartDrawer.tsx
import React from 'react'
import { useCart } from '@faststore/sdk'
import { useSession } from '@faststore/sdk'
import { Button, Loader } from '@faststore/ui'
export default function CartDrawer() {
const { items, totalItems, isValidating, removeItem, updateItemQuantity } =
useCart()
const { currency, locale } = useSession()
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency.code,
})
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
if (totalItems === 0) {
return (
<div data-fs-cart-drawer>
<h2>Your Cart</h2>
<p>Your cart is empty. Start shopping to add items.</p>
</div>
)
}
return (
<div data-fs-cart-drawer>
<h2>Your Cart ({totalItems} items)</h2>
{isValidating && (
<div data-fs-cart-loading>
<Loader />
<span>Verifying prices and availability...</span>
</div>
)}
<ul data-fs-cart-items>
{items.map((item) => (
<li key={item.id} data-fs-cart-item>
<span data-fs-cart-item-name>{item.itemOffered.name}</span>
<span data-fs-cart-item-price>{formatter.format(item.price)}</span>
<div data-fs-cart-item-quantity>
<Button
variant="tertiary"
onClick={() => updateItemQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
>
-
</Button>
<span>{item.quantity}</span>
<Button
variant="tertiary"
onClick={() => updateItemQuantity(item.id, item.quantity + 1)}
>
+
</Button>
</div>
<Button variant="tertiary" onClick={() => removeItem(item.id)}>
Remove
</Button>
</li>
))}
</ul>
<div data-fs-cart-summary>
<span>Subtotal: {formatter.format(subtotal)}</span>
<a
href="/checkout"
data-fs-checkout-button
aria-disabled={isValidating}
onClick={(e) => {
if (isValidating) e.preventDefault()
}}
>
{isValidating ? 'Updating cart...' : 'Proceed to Checkout'}
</a>
</div>
</div>
)
}
useSearch()を使った検索状態:
// src/components/FacetFilter.tsx
import { useSearch } from '@faststore/sdk'
export default function FacetFilter() {
const { state, setState } = useSearch()
const toggleFacet = (facetKey: string, facetValue: string) => {
const currentFacets = state.selectedFacets || []
const exists = currentFacets.some(
(f) => f.key === facetKey && f.value === facetValue
)
const newFacets = exists
? currentFacets.filter(
(f) => !(f.key === facetKey && f.value === facetValue)
)
: [...currentFacets, { key: facetKey, value: facetValue }]
setState({
...state,
selectedFacets: newFacets,
page: 0, // Reset pagination when filters change
})
}
return (
<div data-fs-facet-filter>
<button onClick={() => toggleFacet('brand', 'Nike')}>
Nike {state.selectedFacets?.some((f) => f.key === 'brand' && f.value === 'Nike') ? '✓' : ''}
</button>
</div>
)
}
よくある失敗パターン
- カートの状態に対して独自のReact Context(
CartContext、useReducer)を作成する — VTEXプラットフォームの検証、分析、チェックアウトとの連携が失われます。 - セッションデータ(ロケール、通貨、郵便番号)を
localStorageに保存する — SDKのvalidateSessionミューテーションが実行されず、プラットフォームとの同期が失われます。 useStateで独自の検索状態を構築する — URLとの同期が失われ、戻るボタンによるナビゲーションが機能しなくなり、SDKの最適化されたクエリ生成も利用できません。useCart()のisValidatingフラグを無視する — 価格が古いまま、または在庫切れの商品がある状態で、ユーザーがチェックアウトに進めてしまいます。useCart_unstableまたはuseSession_unstableフックが、変更される可能性のある不安定なインターフェースを持つことを理解せずに使用する。
レビューチェックリスト
- カートの状態は
@faststore/sdkのuseCart()のみで管理されていますか? - セッションデータには
@faststore/sdkのuseSession()のみでアクセスしていますか? - 検索状態は
SearchProviderコンテキスト内でuseSearch()を使って管理されていますか? - チェックアウトへの移動を許可する前に、
isValidatingフラグを確認していますか? - SDKの状態と重複する独自のReact Context、Redux、またはZustandストアはありませんか?
- セッション関連データに対する
localStorage/sessionStorageへの直接アクセスはありませんか?
参考資料
- FastStore SDKの概要 — SDKモジュールとそれぞれの役割の紹介
- useCartフック — すべてのプロパティと関数を含む、カートフックのAPIリファレンス
- Cartモジュールの概要 — カートのデータ構造、検証、プラットフォーム連携
- Sessionモジュール — セッションのデータ構造、通貨、ロケール、チャネルの管理
- useSearchフック — 並べ替え、ファセット、ページネーションを含む、検索フックのAPIリファレンス
- SearchProvider — useSearchの動作に必要なコンテキストプロバイダー
- Analyticsモジュール — GA4互換の分析イベントトラッキング
- 実験的なフックとコンポーネント — 高度なユースケース向けの不安定なフック(useCart_unstable、useSession_unstable)
faststore-data-fetching— GraphQL APIを使った商品データ取得に関する関連スキル