Files
2026-03-07 11:07:45 -03:00

497 lines
13 KiB
Markdown

# Navigation and Routing
## Contents
- [Pre-Implementation Requirements for pnpm](#pre-implementation-requirements-for-pnpm)
- [Basic Navigation with Link Component](#basic-navigation-with-link-component)
- [Programmatic Navigation](#programmatic-navigation)
- [Accessing Route Parameters](#accessing-route-parameters)
- [Linking to Built-in Admin Pages](#linking-to-built-in-admin-pages)
- [Navigation from Widgets](#navigation-from-widgets)
- [Common Navigation Patterns](#common-navigation-patterns)
## Pre-Implementation Requirements for pnpm
**⚠️ pnpm Users**: Navigation requires `react-router-dom`. Install BEFORE implementing:
```bash
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]
```
**npm/yarn Users**: DO NOT install - already available through dashboard dependencies.
## Basic Navigation with Link Component
Use the `Link` component for internal navigation in widgets and custom pages:
```tsx
import { Link } from "react-router-dom"
import { Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
// Link to a custom page
<Link
to="/custom/my-page"
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
>
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-3 transition-colors">
<div className="flex items-center gap-3">
<Text size="small" leading="compact" weight="plus">
Go to Custom Page
</Text>
<div className="size-7 flex items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
</div>
</div>
</div>
</Link>
```
### Link with Dynamic ID
```tsx
// Link to product details
<Link to={`/products/${product.id}`}>
<Text size="small" leading="compact" weight="plus">
{product.title}
</Text>
</Link>
```
### Button-styled Link
```tsx
import { Button } from "@medusajs/ui"
import { Link } from "react-router-dom"
<Button asChild size="small" variant="secondary">
<Link to="/custom/my-page">
View Details
</Link>
</Button>
```
## Programmatic Navigation
Use `useNavigate` for navigation after actions (e.g., after creating an entity):
```tsx
import { useNavigate } from "react-router-dom"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast, Button } from "@medusajs/ui"
import { sdk } from "../lib/client"
const CreateProductWidget = () => {
const navigate = useNavigate()
const queryClient = useQueryClient()
const createProduct = useMutation({
mutationFn: (data) => sdk.admin.product.create(data),
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product created successfully")
// Navigate to the new product's page
navigate(`/products/${result.product.id}`)
},
onError: (error) => {
toast.error(error.message || "Failed to create product")
},
})
const handleCreate = () => {
createProduct.mutate({
title: "New Product",
// ... other fields
})
}
return (
<Button onClick={handleCreate} isLoading={createProduct.isPending}>
Create and View Product
</Button>
)
}
```
### Navigate with State
Pass data to the destination page:
```tsx
navigate("/custom/review", {
state: { productId: product.id, productTitle: product.title }
})
// Access in destination page
import { useLocation } from "react-router-dom"
const ReviewPage = () => {
const location = useLocation()
const { productId, productTitle } = location.state || {}
return <div>Reviewing: {productTitle}</div>
}
```
### Navigate Back
```tsx
const navigate = useNavigate()
<Button onClick={() => navigate(-1)}>
Go Back
</Button>
```
## Accessing Route Parameters
In custom pages, access URL parameters with `useParams`:
```tsx
// Custom page at: /custom/products/:id
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/client"
import { Container, Heading } from "@medusajs/ui"
const ProductDetailsPage = () => {
const { id } = useParams() // Get :id from URL
const { data: product, isLoading } = useQuery({
queryFn: () => sdk.admin.product.retrieve(id, {
fields: "+metadata,+variants.*",
}),
queryKey: ["product", id],
enabled: !!id, // Only fetch if ID exists
})
if (isLoading) return <div>Loading...</div>
return (
<Container>
<Heading>{product?.title}</Heading>
{/* Product details */}
</Container>
)
}
export default ProductDetailsPage
```
### Multiple Parameters
```tsx
// Route: /custom/orders/:orderId/items/:itemId
const { orderId, itemId } = useParams()
const { data } = useQuery({
queryFn: () => sdk.client.fetch(`/admin/orders/${orderId}/items/${itemId}`),
queryKey: ["order-item", orderId, itemId],
enabled: !!orderId && !!itemId,
})
```
### Query Parameters
Use `useSearchParams` for query string parameters:
```tsx
import { useSearchParams } from "react-router-dom"
const ProductsPage = () => {
const [searchParams, setSearchParams] = useSearchParams()
const status = searchParams.get("status") // Get ?status=published
const page = searchParams.get("page") // Get ?page=2
const { data } = useQuery({
queryFn: () => sdk.admin.product.list({
status,
offset: (parseInt(page) || 0) * 15,
}),
queryKey: ["products", status, page],
})
// Update query params
const handleFilterChange = (newStatus: string) => {
setSearchParams({ status: newStatus, page: "0" })
}
return (
<div>
<Button onClick={() => handleFilterChange("published")}>
Published Only
</Button>
{/* Products list */}
</div>
)
}
```
## Linking to Built-in Admin Pages
Link to standard Medusa admin pages:
```tsx
import { Link } from "react-router-dom"
// Product details
<Link to={`/products/${productId}`}>View Product</Link>
// Order details
<Link to={`/orders/${orderId}`}>View Order</Link>
// Customer details
<Link to={`/customers/${customerId}`}>View Customer</Link>
// Product categories
<Link to="/categories">View Categories</Link>
// Settings
<Link to="/settings">Settings</Link>
// Custom field in settings
<Link to="/settings/custom-field-name">Custom Settings</Link>
```
### Common Built-in Routes
```tsx
const ADMIN_ROUTES = {
products: "/products",
productDetails: (id: string) => `/products/${id}`,
orders: "/orders",
orderDetails: (id: string) => `/orders/${id}`,
customers: "/customers",
customerDetails: (id: string) => `/customers/${id}`,
categories: "/categories",
inventory: "/inventory",
pricing: "/pricing",
settings: "/settings",
}
// Usage
<Link to={ADMIN_ROUTES.productDetails(product.id)}>
View Product
</Link>
```
## Navigation from Widgets
### Pattern: View All Link
Add a "View All" link from a widget to a custom page:
```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Text } from "@medusajs/ui"
import { Link } from "react-router-dom"
import { DetailWidgetProps } from "@medusajs/framework/types"
const RelatedProductsWidget = ({ data: product }) => {
// ... widget logic
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Related Products</Heading>
<div className="flex items-center gap-x-2">
<Button size="small" variant="secondary" onClick={() => setOpen(true)}>
Edit
</Button>
<Button asChild size="small" variant="transparent">
<Link to={`/custom/products/${product.id}/related`}>
View All
</Link>
</Button>
</div>
</div>
{/* Widget content */}
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default RelatedProductsWidget
```
### Pattern: List Item Navigation
Make list items clickable to navigate:
```tsx
import { Thumbnail, Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
import { Link } from "react-router-dom"
const ProductListItem = ({ product }) => {
return (
<Link
to={`/products/${product.id}`}
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
>
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-2 transition-colors">
<div className="flex items-center gap-3">
<Thumbnail src={product.thumbnail} />
<div className="flex flex-1 flex-col">
<Text size="small" leading="compact" weight="plus">
{product.title}
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{product.status}
</Text>
</div>
<div className="size-7 flex items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
</div>
</div>
</div>
</Link>
)
}
```
## Common Navigation Patterns
### Pattern: Back to List
Navigate back to list after viewing details:
```tsx
import { useNavigate } from "react-router-dom"
import { ArrowLeft } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"
const DetailsPage = () => {
const navigate = useNavigate()
return (
<div>
<div className="flex items-center gap-x-2 mb-4">
<IconButton onClick={() => navigate("/custom/products")}>
<ArrowLeft />
</IconButton>
<Heading>Product Details</Heading>
</div>
{/* Details content */}
</div>
)
}
```
### Pattern: Breadcrumb Navigation
```tsx
import { Link } from "react-router-dom"
import { Text } from "@medusajs/ui"
const Breadcrumbs = ({ product }) => {
return (
<div className="flex items-center gap-x-2">
<Link to="/products">
<Text size="small" className="text-ui-fg-subtle hover:text-ui-fg-base">
Products
</Text>
</Link>
<Text size="small" className="text-ui-fg-muted">/</Text>
<Link to={`/products/${product.id}`}>
<Text size="small" className="text-ui-fg-subtle hover:text-ui-fg-base">
{product.title}
</Text>
</Link>
<Text size="small" className="text-ui-fg-muted">/</Text>
<Text size="small" weight="plus">Details</Text>
</div>
)
}
```
### Pattern: Tab Navigation
Navigate between different views using tabs:
```tsx
import { useSearchParams, Link } from "react-router-dom"
import { Tabs } from "@medusajs/ui"
const ProductTabs = () => {
const [searchParams] = useSearchParams()
const activeTab = searchParams.get("tab") || "details"
return (
<Tabs value={activeTab}>
<Tabs.List>
<Tabs.Trigger value="details" asChild>
<Link to="?tab=details">Details</Link>
</Tabs.Trigger>
<Tabs.Trigger value="variants" asChild>
<Link to="?tab=variants">Variants</Link>
</Tabs.Trigger>
<Tabs.Trigger value="media" asChild>
<Link to="?tab=media">Media</Link>
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="details">
{/* Details content */}
</Tabs.Content>
<Tabs.Content value="variants">
{/* Variants content */}
</Tabs.Content>
<Tabs.Content value="media">
{/* Media content */}
</Tabs.Content>
</Tabs>
)
}
```
### Pattern: Action with Navigation
Perform an action then navigate:
```tsx
const deleteProduct = useMutation({
mutationFn: (id) => sdk.admin.product.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product deleted")
navigate("/products") // Navigate to list after deletion
},
})
```
### Pattern: Conditional Navigation
Navigate based on state or data:
```tsx
const handleComplete = () => {
if (hasErrors) {
toast.error("Please fix errors first")
return
}
if (isDraft) {
navigate(`/custom/products/${id}/publish`)
} else {
navigate("/products")
}
}
```
## Important Notes
1. **pnpm users**: Must install `react-router-dom` with exact version from dashboard
2. **npm/yarn users**: Do NOT install `react-router-dom` - already available
3. **Always use relative paths** starting with `/` for internal navigation
4. **Use Link for navigation links** - better for SEO and accessibility
5. **Use navigate for programmatic navigation** - after actions or based on logic
6. **Always handle loading states** when fetching route parameter-based data
7. **Clean up on unmount** when using listeners or subscriptions in routes
8. **Maintain focus management** for accessibility when navigating