13 KiB
13 KiB
Navigation and Routing
Contents
- Pre-Implementation Requirements for pnpm
- Basic Navigation with Link Component
- Programmatic Navigation
- Accessing Route Parameters
- Linking to Built-in Admin Pages
- Navigation from Widgets
- Common Navigation Patterns
Pre-Implementation Requirements for pnpm
⚠️ pnpm Users: Navigation requires react-router-dom. Install BEFORE implementing:
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:
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
// Link to product details
<Link to={`/products/${product.id}`}>
<Text size="small" leading="compact" weight="plus">
{product.title}
</Text>
</Link>
Button-styled Link
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):
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:
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
const navigate = useNavigate()
<Button onClick={() => navigate(-1)}>
Go Back
</Button>
Accessing Route Parameters
In custom pages, access URL parameters with useParams:
// 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
// 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:
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:
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
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:
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:
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:
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
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:
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:
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:
const handleComplete = () => {
if (hasErrors) {
toast.error("Please fix errors first")
return
}
if (isDraft) {
navigate(`/custom/products/${id}/publish`)
} else {
navigate("/products")
}
}
Important Notes
- pnpm users: Must install
react-router-domwith exact version from dashboard - npm/yarn users: Do NOT install
react-router-dom- already available - Always use relative paths starting with
/for internal navigation - Use Link for navigation links - better for SEO and accessibility
- Use navigate for programmatic navigation - after actions or based on logic
- Always handle loading states when fetching route parameter-based data
- Clean up on unmount when using listeners or subscriptions in routes
- Maintain focus management for accessibility when navigating