Subchapter 2.10
checkpoints/checkpoint-widget.mdMarkdown10 KBView on GitHub
This checkpoint verifies that you’ve successfully created a widget that displays a product’s brand on the product detail page.
Before proceeding, test your understanding:
What is a widget and how is it different from a UI route?
A widget is a React component injected into an existing admin page at a predefined zone. It extends existing pages without replacing them. A UI route is a completely new page you create. Use widgets when you want to add information to existing pages (like adding brand to product details). Use UI routes when you need a new standalone page (like a brands management page).
Why do we need to refetch product data in the widget when the page already loads the product?
The product detail page doesn’t include linked relations by default (like brand). We need to explicitly request the brand data using the fields parameter. The widget fetches the same product but with fields: "+brand.*" to include the brand relation. React Query caches this, so it’s not inefficient.
What is React Query’s queryKey and why is it important?
queryKey is a unique identifier for a query. React Query uses it for caching, refetching, and invalidation. The key should include all dependencies - in our case, ["product", product.id, "brand"]. If the product ID changes, React Query knows to fetch different data. If you mutate a brand, you can invalidate this key to refetch fresh data.
Why do widgets use Medusa UI components instead of regular HTML/CSS?
Medusa UI components maintain design consistency with the rest of the admin dashboard (colors, spacing, typography, interactions). They’re also accessible and responsive out of the box. Using standard HTML/CSS would make your widget look out of place and require extra styling work.
Let me verify your implementation. Please share the following:
Show me your src/admin/lib/sdk.ts file.
Key things to check:
Medusa from “@medusajs/js-sdk”new Medusa()baseUrl using import.meta.env.VITE_BACKEND_URL or “/”debug: import.meta.env.DEVauth.type: "session"export const sdkShow me your src/admin/widgets/product-brand.tsx file.
Key things to check:
defineWidgetConfig from “@medusajs/admin-sdk”DetailWidgetProps, AdminProduct from “@medusajs/framework/types”Container, Heading, Text from “@medusajs/ui”useQuery from “@tanstack/react-query”sdk from “../lib/sdk”AdminProductBrand type extending AdminProduct with brandDetailWidgetProps<AdminProduct>{ data: product }queryFn calls sdk.admin.product.retrieve() with fields: "+brand.*"queryKey includes product.iddefineWidgetConfig({ zone: "product.details" })Ensure dev server is running with admin:
npm run devExpected: Server starts and admin accessible at http://localhost:9000/app (opens in a new tab)
Expected:
Expected:
Symptom: Build error or runtime error about missing react-query
Cause: pnpm strict dependency resolution
Fix: Find the exact version used by Medusa:
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboardInstall that specific version:
pnpm add @tanstack/react-query@5.x.xSymptom: Navigate to product but no widget appears
Causes and Fixes:
Cause 1: Wrong zone name
"product.details"Cause 2: Config not exported
export const config = defineWidgetConfig({ zone: "product.details" })Cause 3: File not in correct location
src/admin/widgets/product-brand.tsxCause 4: Default export missing
export default ProductBrandWidgetSymptom: Runtime error when accessing brand
Cause: Query result structure not properly typed
Fix: Type the query result properly:
const { data: queryResult } = useQuery({ ... })
const brandName = (queryResult?.product as AdminProductBrand)?.brand?.nameUse optional chaining throughout.
Symptom: Widget shows “-” instead of brand name
Causes and Fixes:
Cause 1: fields parameter incorrect
"+brand.*" (with + sign)Cause 2: Link not created
Cause 3: Extracting brand from wrong location
Symptom: Runtime error about sdk
Cause: SDK not imported or initialized
Fix:
src/admin/lib/sdk.ts (see Implementation Check #1)import { sdk } from "../lib/sdk"Symptom: Widget has different colors, spacing, or font
Cause: Not using Medusa UI components or adding custom CSS
Fix: Use only Medusa UI components:
import { Container, Heading, Text } from "@medusajs/ui"
// Use Container for the widget wrapper
<Container className="divide-y p-0">
// Use Heading for title
<Heading level="h2">Brand</Heading>
// Use Text for content
<Text size="small">{brandName}</Text>
</Container>Symptom: Widget shows after all other sections
Cause: Not a bug. Since Medusa v2.17.2, position within a zone is controlled by the admin user, not the zone name — the .before/.after suffixes are deprecated.
Fix: Keep the unsuffixed zone and reposition the widget in the dashboard’s Editor view (Layout Composer); the arrangement is saved.
export const config = defineWidgetConfig({
zone: "product.details",
})Symptom: Build fails with TS errors about props
Cause: Incorrect prop type
Fix: Use generic DetailWidgetProps:
const ProductBrandWidget = ({
data: product,
}: DetailWidgetProps<AdminProduct>) => {
// ...
}Verify each of these steps:
At this point, you should understand:
Widget injection system:
Admin Product Detail Page
┌────────────────────────────────────┐
│ Page Header │
│ (Medusa Core) │
├────────────────────────────────────┤
│ zone: product.details (main) │
│ │
│ Product Information (Core) │
│ Variants Section (Core) │
│ ┌──────────────────────────────┐ │
│ │ Your Widget: │ │ ← injected into the same zone;
│ │ Brand: Nike │ │ order set by the user in the
│ └──────────────────────────────┘ │ Editor view (Layout Composer)
└────────────────────────────────────┘Why widgets matter:
React Query caching:
Once this checkpoint passes:
The widget enhances the existing product page. Next, we’ll create a completely new admin page for managing all brands in a table with pagination.
Ready to continue? Let me know when all checks pass, and we’ll create the brands management page.