vtex-io-react-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Frontend React Components & Hooks

前端React组件与Hooks

When this skill applies

本技能的适用场景

Use this skill when building VTEX IO frontend apps using the
react
builder — creating React components that integrate with Store Framework as theme blocks, configuring
interfaces.json
, setting up
contentSchemas.json
for Site Editor, and applying styling patterns.
  • Creating custom storefront components (product displays, forms, banners)
  • Building admin panel interfaces with VTEX Styleguide
  • Registering components as Store Framework blocks
  • Exposing component props in Site Editor via
    contentSchemas.json
  • Applying
    css-handles
    for safe storefront styling
Do not use this skill for:
  • Backend service implementation (use
    vtex-io-service-apps
    instead)
  • GraphQL schema and resolver development (use
    vtex-io-graphql-api
    instead)
  • Manifest and builder configuration (use
    vtex-io-app-structure
    instead)
当你使用
react
构建器开发VTEX IO前端应用时,请使用本技能——包括创建与Store Framework集成作为主题区块的React组件、配置
interfaces.json
、为Site Editor设置
contentSchemas.json
,以及应用样式规范。
  • 创建自定义店铺前端组件(商品展示、表单、横幅)
  • 使用VTEX Styleguide构建管理面板界面
  • 将组件注册为Store Framework区块
  • 通过
    contentSchemas.json
    在Site Editor中暴露组件属性
  • 应用
    css-handles
    实现安全的店铺前端样式
请勿在以下场景使用本技能:
  • 后端服务实现(请改用
    vtex-io-service-apps
  • GraphQL schema与解析器开发(请改用
    vtex-io-graphql-api
  • 清单与构建器配置(请改用
    vtex-io-app-structure

Decision rules

决策规则

  • Every visible storefront element is a block. Blocks are declared in theme JSON and map to React components via interfaces.
  • interfaces.json
    (in
    /store
    ) maps block names to React component files:
    "component"
    is the file name in
    /react
    (without extension),
    "allowed"
    lists child blocks,
    "composition"
    controls how children work (
    "children"
    or
    "blocks"
    ).
  • Each exported component MUST have a root-level file in
    /react
    that re-exports it. The builder resolves
    "component": "ProductReviews"
    to
    react/ProductReviews.tsx
    .
  • For storefront components, use
    vtex.css-handles
    for styling (not inline styles, not global CSS).
  • For admin components, use
    vtex.styleguide
    — the official VTEX Admin component library. No third-party UI libraries.
  • Use
    contentSchemas.json
    in
    /store
    to make component props editable in Site Editor (JSON Schema format).
  • Use
    react-intl
    and the
    messages
    builder for i18n — never hardcode user-facing strings.
  • Fetch data via GraphQL queries (
    useQuery
    from
    react-apollo
    ), never via direct API calls from the browser.
Architecture:
text
Store Theme (JSON blocks)
  └── declares "product-reviews" block with props
interfaces.json → maps "product-reviews" to "ProductReviews" component
react/ProductReviews.tsx → React component renders
        ├── useCssHandles() → CSS classes for styling
        ├── useQuery() → GraphQL data fetching
        └── useProduct() / useOrderForm() → Store Framework context hooks
  • 所有可见的店铺前端元素都是一个区块。区块在主题JSON中声明,并通过接口映射到React组件。
  • /store
    目录下的
    interfaces.json
    将区块名称映射到React组件文件:
    "component"
    对应
    /react
    目录下的文件名(不含扩展名),
    "allowed"
    列出允许的子区块,
    "composition"
    控制子元素的工作方式(
    "children"
    "blocks"
    )。
  • 每个导出的组件必须在
    /react
    目录下有一个根级文件重新导出它。构建器会将
    "component": "ProductReviews"
    解析为
    react/ProductReviews.tsx
  • 对于店铺前端组件,请使用
    vtex.css-handles
    进行样式处理(不要使用内联样式或全局CSS)。
  • 对于管理端组件,请使用
    vtex.styleguide
    ——官方VTEX管理端组件库。禁止使用第三方UI库。
  • 使用
    /store
    目录下的
    contentSchemas.json
    使组件属性可在Site Editor中编辑(采用JSON Schema格式)。
  • 使用
    react-intl
    messages
    构建器实现国际化——切勿硬编码面向用户的字符串。
  • 通过GraphQL查询(来自
    react-apollo
    useQuery
    )获取数据,切勿从浏览器直接调用API。
架构:
text
Store Theme (JSON blocks)
  └── declares "product-reviews" block with props
interfaces.json → maps "product-reviews" to "ProductReviews" component
react/ProductReviews.tsx → React component renders
        ├── useCssHandles() → CSS classes for styling
        ├── useQuery() → GraphQL data fetching
        └── useProduct() / useOrderForm() → Store Framework context hooks

Hard constraints

硬性约束

Constraint: Declare Interfaces for All Storefront Blocks

约束:为所有店铺前端区块声明接口

Every React component that should be usable as a Store Framework block MUST have a corresponding entry in
store/interfaces.json
. Without the interface declaration, the block cannot be referenced in theme JSON files.
Why this matters
The store builder resolves block names to React components through
interfaces.json
. If a component has no interface, it is invisible to Store Framework and will not render on the storefront.
Detection
If a React component in
/react
is intended for storefront use but has no matching entry in
store/interfaces.json
, warn the developer. The component will compile but never render.
Correct
json
{
  "product-reviews": {
    "component": "ProductReviews",
    "composition": "children",
    "allowed": ["product-review-item"]
  },
  "product-review-item": {
    "component": "ReviewItem"
  }
}
tsx
// react/ProductReviews.tsx
import ProductReviews from './components/ProductReviews'

export default ProductReviews
Wrong
tsx
// react/ProductReviews.tsx exists but NO store/interfaces.json entry
// The component compiles fine but cannot be used in any theme.
// Adding <product-reviews /> in a theme JSON will produce:
// "Block 'product-reviews' not found"
import ProductReviews from './components/ProductReviews'

export default ProductReviews

每个要作为Store Framework区块使用的React组件,必须在
store/interfaces.json
中有对应的条目。没有接口声明的话,该区块无法在主题JSON文件中被引用。
重要性
店铺构建器通过
interfaces.json
将区块名称解析为React组件。如果组件没有对应的接口,Store Framework将无法识别它,也不会在店铺前端渲染。
检测方式
如果
/react
目录下的React组件用于店铺前端,但在
store/interfaces.json
中没有匹配的条目,请提醒开发者。该组件会正常编译,但永远不会渲染。
正确示例
json
{
  "product-reviews": {
    "component": "ProductReviews",
    "composition": "children",
    "allowed": ["product-review-item"]
  },
  "product-review-item": {
    "component": "ReviewItem"
  }
}
tsx
// react/ProductReviews.tsx
import ProductReviews from './components/ProductReviews'

export default ProductReviews
错误示例
tsx
// react/ProductReviews.tsx存在,但没有对应的store/interfaces.json条目
// 组件会正常编译,但无法在任何主题中使用。
// 在主题JSON中添加<product-reviews />会报错:
// "Block 'product-reviews' not found"
import ProductReviews from './components/ProductReviews'

export default ProductReviews

Constraint: Use VTEX Styleguide for Admin UIs

约束:管理端UI必须使用VTEX Styleguide

Admin panel components (apps using the
admin
builder) MUST use VTEX Styleguide (
vtex.styleguide
) for UI elements. You MUST NOT use third-party UI libraries like Material UI, Chakra UI, or Ant Design in admin apps.
Why this matters
VTEX Admin has a consistent design language enforced by Styleguide. Third-party UI libraries produce inconsistent visuals, may conflict with the Admin's global CSS, and add unnecessary bundle size. Apps submitted to the VTEX App Store with non-Styleguide admin UIs will fail review.
Detection
If you see imports from
@material-ui
,
@chakra-ui/react
,
@chakra-ui
,
antd
, or
@ant-design
in an admin app, warn the developer to use
vtex.styleguide
instead.
Correct
tsx
// react/admin/ReviewModeration.tsx
import React, { useState } from 'react'
import {
  Layout,
  PageHeader,
  Table,
  Button,
  Tag,
  Modal,
  Input,
} from 'vtex.styleguide'

interface Review {
  id: string
  author: string
  rating: number
  text: string
  status: 'pending' | 'approved' | 'rejected'
}

function ReviewModeration() {
  const [reviews, setReviews] = useState<Review[]>([])
  const [modalOpen, setModalOpen] = useState(false)

  const tableSchema = {
    properties: {
      author: { title: 'Author', width: 200 },
      rating: { title: 'Rating', width: 100 },
      text: { title: 'Review Text' },
      status: {
        title: 'Status',
        width: 150,
        cellRenderer: ({ cellData }: { cellData: string }) => (
          <Tag type={cellData === 'approved' ? 'success' : 'error'}>
            {cellData}
          </Tag>
        ),
      },
    },
  }

  return (
    <Layout fullWidth pageHeader={<PageHeader title="Review Moderation" />}>
      <Table
        items={reviews}
        schema={tableSchema}
        density="medium"
      />
    </Layout>
  )
}

export default ReviewModeration
Wrong
tsx
// react/admin/ReviewModeration.tsx
import React from 'react'
import { DataGrid } from '@material-ui/data-grid'
import { Button } from '@material-ui/core'

// Material UI components will look inconsistent in the VTEX Admin,
// conflict with global styles, and inflate bundle size.
// This app will fail VTEX App Store review.
function ReviewModeration() {
  return (
    <div>
      <DataGrid rows={[]} columns={[]} />
      <Button variant="contained" color="primary">Approve</Button>
    </div>
  )
}

管理面板组件(使用
admin
构建器的应用)必须使用VTEX Styleguide(
vtex.styleguide
)作为UI元素。禁止在管理端应用中使用Material UI、Chakra UI或Ant Design等第三方UI库。
重要性
VTEX管理端有由Styleguide强制执行的统一设计语言。第三方UI库会导致视觉不一致,可能与管理端的全局CSS冲突,还会增加不必要的包体积。使用非Styleguide管理端UI的应用在提交到VTEX应用商店时会审核不通过。
检测方式
如果在管理端应用中看到
@material-ui
@chakra-ui/react
@chakra-ui
antd
@ant-design
的导入,请提醒开发者改用
vtex.styleguide
正确示例
tsx
// react/admin/ReviewModeration.tsx
import React, { useState } from 'react'
import {
  Layout,
  PageHeader,
  Table,
  Button,
  Tag,
  Modal,
  Input,
} from 'vtex.styleguide'

interface Review {
  id: string
  author: string
  rating: number
  text: string
  status: 'pending' | 'approved' | 'rejected'
}

function ReviewModeration() {
  const [reviews, setReviews] = useState<Review[]>([])
  const [modalOpen, setModalOpen] = useState(false)

  const tableSchema = {
    properties: {
      author: { title: 'Author', width: 200 },
      rating: { title: 'Rating', width: 100 },
      text: { title: 'Review Text' },
      status: {
        title: 'Status',
        width: 150,
        cellRenderer: ({ cellData }: { cellData: string }) => (
          <Tag type={cellData === 'approved' ? 'success' : 'error'}>
            {cellData}
          </Tag>
        ),
      },
    },
  }

  return (
    <Layout fullWidth pageHeader={<PageHeader title="Review Moderation" />}>
      <Table
        items={reviews}
        schema={tableSchema}
        density="medium"
      />
    </Layout>
  )
}

export default ReviewModeration
错误示例
tsx
// react/admin/ReviewModeration.tsx
import React from 'react'
import { DataGrid } from '@material-ui/data-grid'
import { Button } from '@material-ui/core'

// Material UI组件在VTEX管理端中视觉效果不一致,
// 会与全局样式冲突,还会增大包体积。
// 该应用在VTEX应用商店审核中会不通过。
function ReviewModeration() {
  return (
    <div>
      <DataGrid rows={[]} columns={[]} />
      <Button variant="contained" color="primary">Approve</Button>
    </div>
  )
}

Constraint: Export Components from react/ Root Level

约束:从react/根目录导出组件

Every Store Framework block component MUST have a root-level export file in the
/react
directory that matches the
component
value in
interfaces.json
. The actual implementation can live in subdirectories, but the root file must exist.
Why this matters
The react builder resolves components by looking for files at the root of
/react
. If
interfaces.json
declares
"component": "ProductReviews"
, the builder looks for
react/ProductReviews.tsx
. Without this root export file, the component will not be found and the block will fail to render.
Detection
If
interfaces.json
references a component name that does not have a matching file at the root of
/react
, STOP and create the export file.
Correct
tsx
// react/ProductReviews.tsx — root-level export file
import ProductReviews from './components/ProductReviews/index'

export default ProductReviews
tsx
// react/components/ProductReviews/index.tsx — actual implementation
import React from 'react'
import { useCssHandles } from 'vtex.css-handles'

const CSS_HANDLES = ['container', 'title', 'list'] as const

interface Props {
  title: string
  maxReviews: number
}

function ProductReviews({ title, maxReviews }: Props) {
  const handles = useCssHandles(CSS_HANDLES)
  return (
    <div className={handles.container}>
      <h2 className={handles.title}>{title}</h2>
      {/* ... */}
    </div>
  )
}

export default ProductReviews
Wrong
text
react/components/ProductReviews/index.tsx exists but
react/ProductReviews.tsx does NOT exist.
The builder cannot find the component.
Error: "Could not find component ProductReviews"
每个Store Framework区块组件必须在
/react
目录下有一个根级导出文件,且文件名与
interfaces.json
中的
component
值匹配。实际实现代码可以放在子目录中,但根级导出文件必须存在。
重要性
React构建器通过查找
/react
根目录下的文件来解析组件。如果
interfaces.json
声明了
"component": "ProductReviews"
,构建器会查找
react/ProductReviews.tsx
。没有这个根级导出文件的话,组件会无法被找到,区块渲染会失败。
检测方式
如果
interfaces.json
引用的组件名称在
/react
根目录下没有匹配的文件,请立即创建导出文件。
正确示例
tsx
// react/ProductReviews.tsx — 根级导出文件
import ProductReviews from './components/ProductReviews/index'

export default ProductReviews
tsx
// react/components/ProductReviews/index.tsx — 实际实现代码
import React from 'react'
import { useCssHandles } from 'vtex.css-handles'

const CSS_HANDLES = ['container', 'title', 'list'] as const

interface Props {
  title: string
  maxReviews: number
}

function ProductReviews({ title, maxReviews }: Props) {
  const handles = useCssHandles(CSS_HANDLES)
  return (
    <div className={handles.container}>
      <h2 className={handles.title}>{title}</h2>
      {/* ... */}
    </div>
  )
}

export default ProductReviews
错误示例
text
react/components/ProductReviews/index.tsx存在,但
react/ProductReviews.tsx不存在。
构建器无法找到该组件。
错误提示:"Could not find component ProductReviews"

Preferred pattern

推荐模式

Create the React component inside a subdirectory:
tsx
// react/components/ProductReviews/index.tsx
import React, { useMemo } from 'react'
import { useQuery } from 'react-apollo'
import { useProduct } from 'vtex.product-context'
import { useCssHandles } from 'vtex.css-handles'

import GET_REVIEWS from '../../graphql/getReviews.graphql'
import ReviewItem from './ReviewItem'

const CSS_HANDLES = [
  'reviewsContainer',
  'reviewsTitle',
  'reviewsList',
  'averageRating',
  'emptyState',
] as const

interface Props {
  title?: string
  showAverage?: boolean
  maxReviews?: number
}

function ProductReviews({
  title = 'Customer Reviews',
  showAverage = true,
  maxReviews = 10,
}: Props) {
  const handles = useCssHandles(CSS_HANDLES)
  const productContext = useProduct()
  const productId = productContext?.product?.productId

  const { data, loading, error } = useQuery(GET_REVIEWS, {
    variables: { productId, limit: maxReviews },
    skip: !productId,
  })

  const averageRating = useMemo(() => {
    if (!data?.reviews?.length) return 0

    const sum = data.reviews.reduce(
      (acc: number, review: { rating: number }) => acc + review.rating,
      0
    )

    return (sum / data.reviews.length).toFixed(1)
  }, [data])

  if (loading) return <div className={handles.reviewsContainer}>Loading...</div>
  if (error) return null

  return (
    <div className={handles.reviewsContainer}>
      <h2 className={handles.reviewsTitle}>{title}</h2>

      {showAverage && data?.reviews?.length > 0 && (
        <div className={handles.averageRating}>
          Average: {averageRating} / 5
        </div>
      )}

      {data?.reviews?.length === 0 ? (
        <p className={handles.emptyState}>No reviews yet.</p>
      ) : (
        <ul className={handles.reviewsList}>
          {data.reviews.map((review: { id: string; author: string; rating: number; text: string }) => (
            <ReviewItem key={review.id} review={review} />
          ))}
        </ul>
      )}
    </div>
  )
}

export default ProductReviews
Root export file:
tsx
// react/ProductReviews.tsx
import ProductReviews from './components/ProductReviews'

export default ProductReviews
Block interface:
json
{
  "product-reviews": {
    "component": "ProductReviews",
    "composition": "children",
    "allowed": ["product-review-form"],
    "render": "client"
  }
}
Site Editor schema:
json
{
  "definitions": {
    "ProductReviews": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string",
          "title": "Section Title",
          "description": "Title displayed above the reviews list",
          "default": "Customer Reviews"
        },
        "showAverage": {
          "type": "boolean",
          "title": "Show average rating",
          "default": true
        },
        "maxReviews": {
          "type": "number",
          "title": "Maximum reviews",
          "default": 10,
          "enum": [5, 10, 20, 50]
        }
      }
    }
  }
}
Using the component in a Store Framework theme:
json
{
  "store.product": {
    "children": [
      "product-images",
      "product-name",
      "product-price",
      "buy-button",
      "product-reviews"
    ]
  },
  "product-reviews": {
    "props": {
      "title": "What Our Customers Say",
      "showAverage": true,
      "maxReviews": 20
    }
  }
}
在子目录中创建React组件:
tsx
// react/components/ProductReviews/index.tsx
import React, { useMemo } from 'react'
import { useQuery } from 'react-apollo'
import { useProduct } from 'vtex.product-context'
import { useCssHandles } from 'vtex.css-handles'

import GET_REVIEWS from '../../graphql/getReviews.graphql'
import ReviewItem from './ReviewItem'

const CSS_HANDLES = [
  'reviewsContainer',
  'reviewsTitle',
  'reviewsList',
  'averageRating',
  'emptyState',
] as const

interface Props {
  title?: string
  showAverage?: boolean
  maxReviews?: number
}

function ProductReviews({
  title = 'Customer Reviews',
  showAverage = true,
  maxReviews = 10,
}: Props) {
  const handles = useCssHandles(CSS_HANDLES)
  const productContext = useProduct()
  const productId = productContext?.product?.productId

  const { data, loading, error } = useQuery(GET_REVIEWS, {
    variables: { productId, limit: maxReviews },
    skip: !productId,
  })

  const averageRating = useMemo(() => {
    if (!data?.reviews?.length) return 0

    const sum = data.reviews.reduce(
      (acc: number, review: { rating: number }) => acc + review.rating,
      0
    )

    return (sum / data.reviews.length).toFixed(1)
  }, [data])

  if (loading) return <div className={handles.reviewsContainer}>Loading...</div>
  if (error) return null

  return (
    <div className={handles.reviewsContainer}>
      <h2 className={handles.reviewsTitle}>{title}</h2>

      {showAverage && data?.reviews?.length > 0 && (
        <div className={handles.averageRating}>
          Average: {averageRating} / 5
        </div>
      )}

      {data?.reviews?.length === 0 ? (
        <p className={handles.emptyState}>No reviews yet.</p>
      ) : (
        <ul className={handles.reviewsList}>
          {data.reviews.map((review: { id: string; author: string; rating: number; text: string }) => (
            <ReviewItem key={review.id} review={review} />
          ))}
        </ul>
      )}
    </div>
  )
}

export default ProductReviews
根级导出文件:
tsx
// react/ProductReviews.tsx
import ProductReviews from './components/ProductReviews'

export default ProductReviews
区块接口:
json
{
  "product-reviews": {
    "component": "ProductReviews",
    "composition": "children",
    "allowed": ["product-review-form"],
    "render": "client"
  }
}
Site Editor schema:
json
{
  "definitions": {
    "ProductReviews": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string",
          "title": "Section Title",
          "description": "Title displayed above the reviews list",
          "default": "Customer Reviews"
        },
        "showAverage": {
          "type": "boolean",
          "title": "Show average rating",
          "default": true
        },
        "maxReviews": {
          "type": "number",
          "title": "Maximum reviews",
          "default": 10,
          "enum": [5, 10, 20, 50]
        }
      }
    }
  }
}
在Store Framework主题中使用组件:
json
{
  "store.product": {
    "children": [
      "product-images",
      "product-name",
      "product-price",
      "buy-button",
      "product-reviews"
    ]
  },
  "product-reviews": {
    "props": {
      "title": "What Our Customers Say",
      "showAverage": true,
      "maxReviews": 20
    }
  }
}

Common failure modes

常见失败模式

  • Importing third-party UI libraries for admin apps: Using
    @material-ui/core
    ,
    @chakra-ui/react
    , or
    antd
    conflicts with VTEX Admin's global CSS, produces inconsistent visuals, and will fail App Store review. Use
    vtex.styleguide
    instead.
  • Directly calling APIs from React components: Using
    fetch()
    or
    axios
    exposes authentication tokens to the client and bypasses CORS restrictions. Use GraphQL queries that resolve server-side via
    useQuery
    from
    react-apollo
    .
  • Hardcoded strings without i18n: Components with hardcoded strings only work in one language. Use the
    messages
    builder and
    react-intl
    for internationalization.
  • Missing root-level export file: If
    interfaces.json
    references
    "component": "ProductReviews"
    but
    react/ProductReviews.tsx
    doesn't exist, the block silently fails to render.
  • 管理端应用导入第三方UI库:使用
    @material-ui/core
    @chakra-ui/react
    antd
    会与VTEX管理端的全局CSS冲突,导致视觉效果不一致,且在应用商店审核中不通过。请改用
    vtex.styleguide
  • 从React组件直接调用API:使用
    fetch()
    axios
    会将认证令牌暴露给客户端,还会绕过CORS限制。请使用通过服务器端解析的GraphQL查询(来自
    react-apollo
    useQuery
    )。
  • 未做国际化的硬编码字符串:含有硬编码字符串的组件只能在一种语言环境下使用。请使用
    messages
    构建器和
    react-intl
    实现国际化。
  • 缺少根级导出文件:如果
    interfaces.json
    引用了
    "component": "ProductReviews"
    react/ProductReviews.tsx
    不存在,区块会静默渲染失败。

Review checklist

审核清单

  • Does every storefront block have a matching entry in
    store/interfaces.json
    ?
  • Does every
    interfaces.json
    component have a root-level export file in
    /react
    ?
  • Are admin apps using
    vtex.styleguide
    (no third-party UI libraries)?
  • Are storefront components using
    css-handles
    for styling?
  • Is data fetched via GraphQL (
    useQuery
    ), not direct API calls?
  • Are user-facing strings using
    react-intl
    and the
    messages
    builder?
  • Is
    contentSchemas.json
    defined for Site Editor-editable props?
  • 每个店铺前端区块在
    store/interfaces.json
    中都有对应的条目吗?
  • 每个
    interfaces.json
    中的组件在
    /react
    目录下都有根级导出文件吗?
  • 管理端应用是否使用了
    vtex.styleguide
    (未使用第三方UI库)?
  • 店铺前端组件是否使用了
    css-handles
    进行样式处理?
  • 是否通过GraphQL(
    useQuery
    )获取数据,而非直接调用API?
  • 面向用户的字符串是否使用了
    react-intl
    messages
    构建器?
  • 是否为可在Site Editor中编辑的属性定义了
    contentSchemas.json

Reference

参考资料