mcpField
F
faststore-state-management

faststore-state-management

github.com/vtex开发4781 次安装2 周前更新
代码仓库

关于 faststore-state-management

faststore-state-management 是面向 FastStore 电商交互开发的状态管理技能,指导开发者使用 @faststore/sdk 管理购物车、会话和搜索。它强调平台同步、购物车验证以及搜索状态与 URL 的联动。

主要功能

  1. 01使用 useCart 管理购物车操作
  2. 02使用 useSession 管理会话数据
  3. 03使用 useSearch 管理搜索状态
  4. 04结账前处理购物车验证状态
  5. 05使用 SDK 发送电商分析事件

适用场景

  1. 01添加、移除或更新购物车商品
  2. 02更改货币、语言或销售渠道
  3. 03实现筛选、排序与分页
  4. 04阻止验证中的购物车进入结账

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vtex/skills --skill faststore-state-management

安装后在 Claude Code 中输入/faststore-state-management确认已加载。

详细介绍

FastStore SDK 状态管理

适用场景

在以下情况下使用此技能:

  • 构建涉及购物车、用户会话、商品搜索/筛选或分析跟踪的交互式电商功能。
  • 需要添加、移除或更新购物车商品。
  • 需要读取或更改会话数据(货币、区域设置、销售渠道、邮政编码)。
  • 需要管理分面搜索状态(排序顺序、选中的分面、分页)。
  • 使用 @faststore/sdk 钩子(useCart、useSession、useSearch)。

以下情况不要使用此技能:

  • 仅更改视觉效果——请使用 faststore-theming 技能。
  • 替换或自定义原生组件——请使用 faststore-overrides 技能。
  • 扩展 GraphQL 架构或获取自定义数据——请使用 faststore-data-fetching 技能。

决策规则

  • 所有购物车操作都使用 useCart()——它会自动处理平台验证、价格核验和分析。
  • 所有会话数据都使用 useSession()——它会触发 validateSession mutations,使平台保持同步。
  • 所有搜索状态都在 SearchProvider 中使用 useSearch()——它会与 URL 参数同步,并支持浏览器后退按钮导航。
  • 不要为购物车、会话或搜索构建自定义状态管理(React Context、Redux、Zustand、useState/useReducer)。SDK 钩子已接入 FastStore 平台集成层。
  • 始终检查 useCart() 中的 isValidating,并在验证期间阻止跳转到结账页面。
  • 使用 SDK 中的 sendAnalyticsEvent() 跟踪兼容 GA4 的电商事件。

硬性约束

约束:购物车、会话和搜索状态必须使用 @faststore/sdk

管理购物车、会话和搜索状态时,必须使用 @faststore/sdk 钩子(useCart、useSession、useSearch)。不得为这些领域构建自定义状态管理(React Context、Redux、Zustand、useState/useReducer)。

为什么这很重要 SDK 钩子已接入 FastStore 平台集成层。useCart() 会触发购物车验证 mutations。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 mutation,以更新服务端会话并重新验证购物车。直接写入 localStorage 会绕过此验证——平台不会获知此项变更,价格可能会以错误的货币显示,购物车商品也可能无法反映正确的销售渠道。

检测 如果看到使用 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 Hooks:

// 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 mutation 不会触发,导致平台状态不同步。
  • 使用 useState 构建自定义搜索状态——这会失去 URL 同步,破坏浏览器后退按钮导航,并绕过 SDK 优化的查询生成。
  • 忽略 useCart() 返回的 isValidating 标志——用户可能会在价格过时或商品缺货时继续结账。
  • 使用 useCart_unstable 或 useSession_unstable Hooks,却不了解它们的接口不稳定,可能会发生变化。

审核清单

  • 是否仅通过 @faststore/sdk 中的 useCart() 管理购物车状态?
  • 是否仅通过 @faststore/sdk 中的 useSession() 访问会话数据?
  • 是否在 SearchProvider 上下文中通过 useSearch() 管理搜索状态?
  • 是否在允许导航至结账前检查 isValidating 标志?
  • 是否未使用自定义 React Context、Redux 或 Zustand Store 重复存储 SDK 状态?
  • 是否未直接通过 localStorage/sessionStorage 访问会话相关数据?

参考

评论

支持 Markdown