# Data Loading Principles and Patterns

## Contents
- [Fundamental Rules](#fundamental-rules)
- [Think Before You Code Checklist](#think-before-you-code-checklist)
- [Common Mistake vs Correct Pattern](#common-mistake-vs-correct-pattern)
- [Working with Tanstack Query](#working-with-tanstack-query)
- [Fetching Data with useQuery](#fetching-data-with-usequery)
  - [Basic Query](#basic-query)
  - [Paginated Query](#paginated-query)
  - [Query with Dependencies](#query-with-dependencies)
  - [Fetching Multiple Items by IDs](#fetching-multiple-items-by-ids)
- [Updating Data with useMutation](#updating-data-with-usemutation)
  - [Basic Mutation](#basic-mutation)
  - [Mutation with Loading State](#mutation-with-loading-state)
  - [Create Mutation](#create-mutation)
  - [Delete Mutation](#delete-mutation)
- [Cache Invalidation Guidelines](#cache-invalidation-guidelines)
- [Important Notes about Metadata](#important-notes-about-metadata)
- [Common Patterns](#common-patterns)
  - [Pattern: Fetching Data with Pagination](#pattern-fetching-data-with-pagination)
  - [Pattern: Search with Debounce](#pattern-search-with-debounce)
  - [Pattern: Updating Metadata with useMutation](#pattern-updating-metadata-with-usemutation)
- [Common Issues & Solutions](#common-issues--solutions)
- [Complete Example: Widget with Separate Queries](#complete-example-widget-with-separate-queries)

## Fundamental Rules

1. **ALWAYS use the Medusa JS SDK** - NEVER use regular fetch() for API requests (missing headers causes authentication/authorization errors)
2. **Display data must load on mount** - Any data shown in the widget's main UI must be fetched when the component mounts, not conditionally
3. **Separate concerns** - Modal/form data queries should be independent from display data queries
4. **Handle reference data properly** - When storing IDs/references (in metadata or elsewhere), you must fetch the full entities to display them
5. **Always show loading states** - Users should see loading indicators, not empty states, while data is being fetched
6. **Invalidate the right queries** - After mutations, invalidate the queries that provide display data, not just the modal queries

## Think Before You Code Checklist

Before implementing any widget that displays data:

- [ ] Am I using the Medusa JS SDK for all API requests (not regular fetch)?
- [ ] For built-in endpoints, am I using existing SDK methods (not sdk.client.fetch)?
- [ ] What data needs to be visible immediately?
- [ ] Where is this data stored? (metadata, separate endpoint, related entities)
- [ ] If storing IDs, how will I fetch the full entities for display?
- [ ] Are my display queries separate from interaction queries?
- [ ] Have I added loading states for all data fetches?
- [ ] Which queries need invalidation after updates to refresh the display?

## Common Mistake vs Correct Pattern

### ❌ WRONG - Single query for both display and modal:

```tsx
// This breaks on page refresh!
const { data } = useQuery({
  queryFn: () => sdk.admin.product.list(),
  enabled: modalOpen, // Display won't work on mount!
})

// Trying to display filtered data from modal query
const displayItems = data?.filter((item) => ids.includes(item.id)) // No data until modal opens
```

**Why this is wrong:**
- On page refresh, modal is closed, so query doesn't run
- User sees empty state instead of their data
- Display depends on modal interaction

### ✅ CORRECT - Separate queries with proper invalidation:

```tsx
// Display data - loads immediately
const { data: displayData } = useQuery({
  queryFn: () => fetchDisplayData(),
  queryKey: ["display-data", product.id],
  // No 'enabled' condition - loads on mount
})

// Modal data - loads when needed
const { data: modalData } = useQuery({
  queryFn: () => fetchModalData(),
  queryKey: ["modal-data"],
  enabled: modalOpen, // OK for modal-only data
})

// Mutation with proper cache invalidation
const updateMutation = useMutation({
  mutationFn: updateFunction,
  onSuccess: () => {
    // Invalidate display data query to refresh UI
    queryClient.invalidateQueries({ queryKey: ["display-data", product.id] })
    // Also invalidate the entity if it caches the data
    queryClient.invalidateQueries({ queryKey: ["product", product.id] })
  },
})
```

**Why this is correct:**
- Display query runs immediately on component mount
- Modal query only runs when needed
- Proper invalidation ensures UI updates after changes
- Each query has a clear, separate responsibility

## Using the Medusa JS SDK

**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()**

### Why the SDK is Required

- **Admin routes** require `Authorization` header and session cookie - SDK adds them automatically
- **Store routes** require `x-publishable-api-key` header - SDK adds them automatically
- **Regular fetch()** doesn't include these headers → authentication/authorization errors
- Using existing SDK methods provides better type safety and autocomplete

### When to Use What

```tsx
import { sdk } from "../lib/client"

// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const product = await sdk.admin.product.retrieve(productId, {
  fields: "+metadata,+variants.*"
})

// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const reviews = await sdk.client.fetch(`/admin/products/${productId}/reviews`)

// ❌ WRONG - Using regular fetch for ANY endpoint
const response = await fetch(`http://localhost:9000/admin/products/${productId}`)
// ❌ Error: Missing Authorization header!
```

### SDK Method Selection

**For built-in Medusa endpoints:**
- Use existing SDK methods: `sdk.admin.product.list()`, `sdk.store.product.list()`, etc.
- Provides type safety, autocomplete, and proper header handling
- Reference: [Medusa JS SDK Documentation](https://docs.medusajs.com/resources/medusa-js-sdk)

**For custom API routes:**
- Use `sdk.client.fetch()` for your custom endpoints
- SDK still handles all required headers (auth, API keys)
- Pass plain objects to body (SDK handles JSON serialization)

## Working with Tanstack Query

Admin widgets and routes have Tanstack Query pre-configured.

**⚠️ pnpm Users**: You MUST install `@tanstack/react-query` BEFORE using `useQuery` or `useMutation`. Install with exact version from dashboard:

```bash
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
pnpm add @tanstack/react-query@[exact-version]
```

**npm/yarn Users**: DO NOT install `@tanstack/react-query` - it's already available through dashboard dependencies.

## Fetching Data with useQuery

### Basic Query

```tsx
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/client"

const { data, isLoading, error } = useQuery({
  queryFn: () => sdk.admin.product.retrieve(productId, {
    fields: "+metadata,+variants.*",
  }),
  queryKey: ["product", productId],
})
```

### Paginated Query

```tsx
const limit = 15
const offset = pagination.pageIndex * limit

const { data: products } = useQuery({
  queryFn: () =>
    sdk.admin.product.list({
      limit,
      offset,
      q: searchTerm, // for search
    }),
  queryKey: ["products", limit, offset, searchTerm],
  keepPreviousData: true, // Prevents UI flicker during pagination
})
```

### Query with Dependencies

```tsx
// Only fetch if productId exists
const { data } = useQuery({
  queryFn: () => sdk.admin.product.retrieve(productId),
  queryKey: ["product", productId],
  enabled: !!productId, // Only run when productId is truthy
})
```

### Fetching Multiple Items by IDs

```tsx
// For display - fetch specific items by IDs
const { data: displayProducts } = useQuery({
  queryFn: async () => {
    if (selectedIds.length === 0) return { products: [] }

    const response = await sdk.admin.product.list({
      id: selectedIds, // Fetch only the selected products
      limit: selectedIds.length,
    })
    return response
  },
  queryKey: ["related-products-display", selectedIds],
  enabled: selectedIds.length > 0, // Only fetch if there are IDs
})
```

## Updating Data with useMutation

### Basic Mutation

```tsx
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "@medusajs/ui"

const queryClient = useQueryClient()

const updateProduct = useMutation({
  mutationFn: (payload) => sdk.admin.product.update(productId, payload),
  onSuccess: () => {
    // Invalidate and refetch
    queryClient.invalidateQueries({ queryKey: ["product", productId] })
    toast.success("Product updated successfully")
  },
  onError: (error) => {
    toast.error(error.message || "Failed to update product")
  },
})

// Usage
const handleSave = () => {
  updateProduct.mutate({
    metadata: {
      ...existingMetadata,
      new_field: "value",
    },
  })
}
```

### Mutation with Loading State

```tsx
<Button
  onClick={handleSave}
  isLoading={updateProduct.isPending}
>
  Save
</Button>
```

### Create Mutation

```tsx
const createProduct = useMutation({
  mutationFn: (data) => sdk.admin.product.create(data),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["products"] })
    toast.success("Product created successfully")
    setOpen(false)
  },
})
```

### Delete Mutation

```tsx
const deleteProduct = useMutation({
  mutationFn: (id) => sdk.admin.product.delete(id),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["products"] })
    toast.success("Product deleted")
  },
})
```

## Cache Invalidation Guidelines

After mutations, invalidate the queries that affect what the user sees:

```tsx
onSuccess: () => {
  // Invalidate the entity itself if it stores the data
  queryClient.invalidateQueries({ queryKey: ["product", productId] })

  // Invalidate display-specific queries
  queryClient.invalidateQueries({ queryKey: ["related-products", productId] })

  // Don't need to invalidate modal selection queries
  // queryClient.invalidateQueries({ queryKey: ["products-list"] }) // Not needed
}
```

**Key Points:**

- Use specific query keys with IDs for targeted invalidation
- Invalidate both the entity and display data queries when needed
- Consider what the user sees and ensure those queries refresh
- Modal/selection queries typically don't need invalidation

## Important Notes about Metadata

- When updating nested objects in metadata, pass the entire object (Medusa doesn't merge nested objects)
- To remove a metadata property, set it to an empty string
- Metadata is stored as JSONB in the database

**Example: Updating Metadata**

```tsx
// ✅ CORRECT - Spread existing metadata
updateProduct.mutate({
  metadata: {
    ...product.metadata,
    new_field: "value",
  },
})

// ❌ WRONG - Overwrites all metadata
updateProduct.mutate({
  metadata: {
    new_field: "value", // All other fields lost!
  },
})
```

## Common Patterns

### Pattern: Fetching Data with Pagination

```tsx
const limit = 15
const offset = pagination.pageIndex * limit

const { data } = useQuery({
  queryFn: () => sdk.admin.product.list({ limit, offset }),
  queryKey: ["products", limit, offset],
  keepPreviousData: true, // Prevents UI flicker during pagination
})
```

### Pattern: Search with Debounce

```tsx
import { useDebouncedValue } from "@mantine/hooks" // or implement your own

const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 300)

const { data } = useQuery({
  queryFn: () => sdk.admin.product.list({ q: debouncedSearch }),
  queryKey: ["products", debouncedSearch],
})
```

### Pattern: Updating Metadata with useMutation

```tsx
const updateMetadata = useMutation({
  mutationFn: (metadata) => sdk.admin.product.update(productId, { metadata }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["product", productId] })
    toast.success("Updated successfully")
  },
})
```

## Common Issues & Solutions

### Authentication/Authorization errors when fetching data

**Symptoms:**
- API returns 401 Unauthorized or 403 Forbidden
- "Missing x-publishable-api-key header" error
- "Unauthorized" error on admin routes

**Cause:** Using regular `fetch()` instead of the Medusa JS SDK

**Solution:**

```tsx
// ❌ WRONG - Missing required headers
const { data } = useQuery({
  queryFn: () => fetch('http://localhost:9000/admin/products').then(r => r.json()),
  queryKey: ["products"]
})

// ✅ CORRECT - SDK handles headers automatically
const { data } = useQuery({
  queryFn: () => sdk.admin.product.list(),
  queryKey: ["products"]
})

// ✅ CORRECT - For custom routes
const { data } = useQuery({
  queryFn: () => sdk.client.fetch('/admin/custom-route'),
  queryKey: ["custom-data"]
})
```

### "No QueryClient set, use QueryClientProvider to set one"

- **pnpm users**: You forgot to install `@tanstack/react-query` before implementing. Install it now with the exact version from dashboard
- **npm/yarn users**: You incorrectly installed `@tanstack/react-query` - remove it from package.json
- Never wrap your component in QueryClientProvider - it's already provided

### Search not filtering results

- The search happens server-side via the `q` parameter
- Make sure to pass the search value in your queryFn:

```tsx
queryFn: () => sdk.admin.product.list({ q: searchValue })
```

### Metadata updates not working

- Always pass the complete metadata object (partial updates aren't merged)
- To remove a field, set it to an empty string, not null or undefined

### Widget not refreshing after mutation

- Use queryClient.invalidateQueries() with the correct query key
- Ensure your query key includes all dependencies (search, pagination, etc.)

### Data shows empty on page refresh

- Your query has `enabled: modalOpen` or similar condition
- Display data should NEVER be conditionally enabled based on UI state
- Move conditional queries to modals/forms only

## Complete Example: Widget with Separate Queries

```tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { useState, useMemo } from "react"
import { Container, Heading, Button, FocusModal, toast } from "@medusajs/ui"
import { sdk } from "../lib/client"

const RelatedProductsWidget = ({ data: product }) => {
  const [open, setOpen] = useState(false)
  const queryClient = useQueryClient()

  // Parse existing related product IDs from metadata
  const relatedIds = useMemo(() => {
    if (product?.metadata?.related_product_ids) {
      try {
        const ids = JSON.parse(product.metadata.related_product_ids)
        return Array.isArray(ids) ? ids : []
      } catch {
        return []
      }
    }
    return []
  }, [product?.metadata?.related_product_ids])

  // Query 1: Fetch selected products for display (loads on mount)
  const { data: displayProducts } = useQuery({
    queryFn: async () => {
      if (relatedIds.length === 0) return { products: [] }
      const response = await sdk.admin.product.list({
        id: relatedIds,
        limit: relatedIds.length,
      })
      return response
    },
    queryKey: ["related-products-display", relatedIds],
    enabled: relatedIds.length > 0,
  })

  // Query 2: Fetch products for modal selection (only when modal is open)
  const { data: modalProducts, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list({ limit: 10, offset: 0 }),
    queryKey: ["products-selection"],
    enabled: open, // Only load when modal is open
  })

  // Mutation to update the product metadata
  const updateProduct = useMutation({
    mutationFn: (relatedProductIds) => {
      return sdk.admin.product.update(product.id, {
        metadata: {
          ...product.metadata,
          related_product_ids: JSON.stringify(relatedProductIds),
        },
      })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["product", product.id] })
      queryClient.invalidateQueries({ queryKey: ["related-products-display"] })
      toast.success("Related products updated")
      setOpen(false)
    },
  })

  return (
    <Container>
      <div className="flex items-center justify-between">
        <Heading>Related Products</Heading>
        <Button onClick={() => setOpen(true)}>Edit</Button>
      </div>

      {/* Display current selection */}
      <div>
        {displayProducts?.products.map((p) => (
          <div key={p.id}>{p.title}</div>
        ))}
      </div>

      {/* Modal for selection */}
      <FocusModal open={open} onOpenChange={setOpen}>
        {/* Modal content with selection UI */}
      </FocusModal>
    </Container>
  )
}
```
