# Checkpoint 3.1: Product Brand Widget
This checkpoint verifies that you've successfully created a widget that displays a product's brand on the product detail page.
## Verification Questions
Before proceeding, test your understanding:
1. **What is a widget and how is it different from a UI route?**
Answer
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).
2. **Why do we need to refetch product data in the widget when the page already loads the product?**
Answer
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.
3. **What is React Query's `queryKey` and why is it important?**
Answer
`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.
4. **Why do widgets use Medusa UI components instead of regular HTML/CSS?**
Answer
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.
## Implementation Check
Let me verify your implementation. Please share the following:
### 1. SDK Configuration
Show me your `src/admin/lib/sdk.ts` file.
**Key things to check**:
- [ ] Imports `Medusa` from "@medusajs/js-sdk"
- [ ] Creates SDK instance with `new Medusa()`
- [ ] Configures `baseUrl` using `import.meta.env.VITE_BACKEND_URL` or "/"
- [ ] Sets `debug: import.meta.env.DEV`
- [ ] Sets `auth.type: "session"`
- [ ] Exports as `export const sdk`
### 2. Widget Component File
Show me your `src/admin/widgets/product-brand.tsx` file.
**Key things to check**:
- [ ] Imports `defineWidgetConfig` from "@medusajs/admin-sdk"
- [ ] Imports types: `DetailWidgetProps`, `AdminProduct` from "@medusajs/framework/types"
- [ ] Imports UI components: `Container`, `Heading`, `Text` from "@medusajs/ui"
- [ ] Imports `useQuery` from "@tanstack/react-query"
- [ ] Imports `sdk` from "../lib/sdk"
- [ ] Defines `AdminProductBrand` type extending `AdminProduct` with brand
- [ ] Component props: `DetailWidgetProps`
- [ ] Destructures product: `{ data: product }`
- [ ] useQuery configuration:
- `queryFn` calls `sdk.admin.product.retrieve()` with `fields: "+brand.*"`
- `queryKey` includes product.id
- [ ] Handles loading state
- [ ] Displays brand name or "-" if no brand
- [ ] Uses Container, Heading, and Text components
- [ ] Exports config: `defineWidgetConfig({ zone: "product.details" })`
- [ ] Default exports component
### 3. Dev Server Running
Ensure dev server is running with admin:
```bash
npm run dev
```
**Expected**: Server starts and admin accessible at http://localhost:9000/app
### 4. Manual Test in Browser
1. Open admin dashboard: http://localhost:9000/app
2. Navigate to Products page
3. Click on a product that has a brand (created in Lesson 2)
4. Look for the Brand widget at the TOP of the product details page
**Expected**:
- Widget appears with heading "Brand"
- Shows brand name (e.g., "Nike")
- Widget styling matches other admin widgets
### 5. Test with Product Without Brand
1. Navigate to a product that doesn't have a brand
2. Check the widget
**Expected**:
- Widget still appears
- Shows "-" for the brand name (indicating no brand)
## Common Issues
### "Cannot find module '@tanstack/react-query'" (pnpm users)
**Symptom**: Build error or runtime error about missing react-query
**Cause**: pnpm strict dependency resolution
**Fix**:
Find the exact version used by Medusa:
```bash
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
```
Install that specific version:
```bash
pnpm add @tanstack/react-query@5.x.x
```
### Widget not showing on product page
**Symptom**: Navigate to product but no widget appears
**Causes and Fixes**:
**Cause 1**: Wrong zone name
- **Fix**: Use exact zone: `"product.details"`
**Cause 2**: Config not exported
- **Fix**: Ensure you export config:
```typescript
export const config = defineWidgetConfig({ zone: "product.details" })
```
**Cause 3**: File not in correct location
- **Fix**: Ensure file is at `src/admin/widgets/product-brand.tsx`
**Cause 4**: Default export missing
- **Fix**: Ensure component is default exported:
```typescript
export default ProductBrandWidget
```
### "Cannot read property 'brand' of undefined"
**Symptom**: Runtime error when accessing brand
**Cause**: Query result structure not properly typed
**Fix**:
Type the query result properly:
```typescript
const { data: queryResult } = useQuery({ ... })
const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name
```
Use optional chaining throughout.
### Brand shows as "-" even though product has brand
**Symptom**: Widget shows "-" instead of brand name
**Causes and Fixes**:
**Cause 1**: fields parameter incorrect
- **Fix**: Use `"+brand.*"` (with + sign)
**Cause 2**: Link not created
- **Fix**: Verify link exists (see Checkpoint 2.2)
**Cause 3**: Extracting brand from wrong location
- **Fix**: Check structure of queryResult
### "sdk is not defined"
**Symptom**: Runtime error about sdk
**Cause**: SDK not imported or initialized
**Fix**:
1. Create `src/admin/lib/sdk.ts` (see Implementation Check #1)
2. Import in widget: `import { sdk } from "../lib/sdk"`
### Widget styling looks wrong / doesn't match dashboard
**Symptom**: Widget has different colors, spacing, or font
**Cause**: Not using Medusa UI components or adding custom CSS
**Fix**:
Use only Medusa UI components:
```typescript
import { Container, Heading, Text } from "@medusajs/ui"
// Use Container for the widget wrapper
// Use Heading for title
Brand
// Use Text for content
{brandName}
```
### Widget appears at bottom instead of top
**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.
```typescript
export const config = defineWidgetConfig({
zone: "product.details",
})
```
### TypeScript errors about widget props
**Symptom**: Build fails with TS errors about props
**Cause**: Incorrect prop type
**Fix**:
Use generic DetailWidgetProps:
```typescript
const ProductBrandWidget = ({
data: product,
}: DetailWidgetProps) => {
// ...
}
```
## Testing Checklist
Verify each of these steps:
- [ ] SDK initialized in src/admin/lib/sdk.ts
- [ ] Widget file created in src/admin/widgets/
- [ ] Widget appears on product detail page (anywhere in the main section)
- [ ] Shows brand name for products with brands
- [ ] Shows "-" for products without brands
- [ ] Styling matches other admin widgets
- [ ] No console errors in browser DevTools
## Architecture Understanding
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**:
- **Non-invasive**: Extend pages without modifying core code
- **Composable**: Multiple widgets can use the same zone
- **User-arrangeable**: Admin users reorder page components, including widgets, in the Editor view (v2.17.2+)
- **Upgrade safe**: Core page updates don't break your widgets
- **Contextual**: Receive page data as props
**React Query caching**:
- First visit: Fetches product with brand
- Navigate away and back: Uses cached data (instant)
- Background refetch: Keeps data fresh
- Mutation: Invalidate cache to trigger refetch
## Next Steps
Once this checkpoint passes:
1. **SDK** initialized for API calls
2. **Product Brand Widget** showing brand on product page
3. **Next**: Create Brands UI Route (Part 3 of Lesson 3)
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.