Skip to main content

Extending the Dashboard

This guide covers the core concepts and best practices for extending the Vendure Dashboard. Understanding these fundamentals will help you build robust and maintainable dashboard extensions.

Dev Mode

Once you have logged in to the dashboard, you can toggle on "Dev Mode" using the user menu in the bottom left:

Dev Mode

In Dev Mode, hovering any block in the dashboard will allow you to find the corresponding pageId and blockId values, which you can later use when customizing the dashboard. This is essential for:

  • Identifying where to place custom page blocks
  • Finding action bar locations
  • Understanding the page structure
  • Debugging your extensions

Finding the location ids

While you can organize your dashboard extensions however you prefer (it's a standard React application), we recommend following this convention for consistency and maintainability:

src/plugins/my-plugin/
└── dashboard/
├── index.tsx # Main entrypoint linked in plugin decorator
├── pages/ # Top-level page components
├── routes/ # Route definitions
├── form-components/ # Input, custom fields, and display components
├── detail-forms/ # Detail form definitions
└── action-bar/ # Action bar items

Entry Point (index.tsx)

The main entry point that is linked in your plugin decorator:

src/plugins/my-plugin/dashboard/index.tsx
import { defineDashboardExtension } from '@vendure/dashboard';

export default defineDashboardExtension({
routes: [],
navSections: [],
pageBlocks: [],
actionBarItems: [],
alerts: [],
widgets: [],
customFormComponents: {},
dataTables: [],
detailForms: [],
login: {},
});
tip

This folder structure is particularly important when open-sourcing Vendure plugins. Following the official conventions makes it easier for other developers to understand and contribute to your plugin.

Form Handling

Form handling in the dashboard is powered by react-hook-form, which is also the foundation for Shadcn's form components. This provides:

  • Excellent performance with minimal re-renders
  • Built-in validation
  • TypeScript support
  • Easy integration with the dashboard's UI components

Basic Form Example

import { useForm } from 'react-hook-form';
import { Form, FormFieldWrapper, Input, Button } from '@vendure/dashboard';

function MyForm() {
const form = useForm({
defaultValues: {
name: '',
email: '',
},
});

const onSubmit = data => {
console.log(data);
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormFieldWrapper
control={form.control}
name="name"
label="Name"
render={({ field }) => <Input {...field} />}
/>
<FormFieldWrapper
control={form.control}
name="email"
label="Email"
render={({ field }) => <Input type="email" {...field} />}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}

Advanced Example

For a comprehensive example of advanced form handling, including complex validation, dynamic fields, and custom components, check out the order detail page implementation in the Vendure source code.

API Client

The API client is the primary way to send queries and mutations to the Vendure backend. It handles channel tokens and authentication automatically.

Importing the API Client

import { api } from '@vendure/dashboard';

The API client exposes two main methods:

  • query - For GraphQL queries
  • mutate - For GraphQL mutations

Using with TanStack Query

The API client is designed to work seamlessly with TanStack Query for optimal data fetching and caching:

Query Example

import { useQuery } from '@tanstack/react-query';
import { api } from '@vendure/dashboard';
import { graphql } from '@/gql';

const getProductsQuery = graphql(`
query GetProducts($options: ProductListOptions) {
products(options: $options) {
items {
id
name
slug
}
totalItems
}
}
`);

function ProductList() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () =>
api.query(getProductsQuery, {
options: {
take: 10,
skip: 0,
},
}),
});

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return <ul>{data?.products.items.map(product => <li key={product.id}>{product.name}</li>)}</ul>;
}

Mutation Example

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@vendure/dashboard';
import { graphql } from '@/gql';
import { toast } from 'sonner';

const updateProductMutation = graphql(`
mutation UpdateProduct($input: UpdateProductInput!) {
updateProduct(input: $input) {
id
name
slug
}
}
`);

function ProductForm({ product }) {
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: input => api.mutate(updateProductMutation, { input }),
onSuccess: () => {
// Invalidate and refetch product queries
queryClient.invalidateQueries({ queryKey: ['products'] });
toast.success('Product updated successfully');
},
onError: error => {
toast.error('Failed to update product', {
description: error.message,
});
},
});

const handleSubmit = data => {
mutation.mutate({
id: product.id,
...data,
});
};

return (
// Form implementation
<form onSubmit={handleSubmit}>{/* Form fields */}</form>
);
}

Best Practices

  1. Follow the folder structure: It helps maintain consistency, especially when sharing plugins
  2. Use TypeScript: Take advantage of the generated GraphQL types for type safety
  3. Leverage TanStack Query: Use it for all data fetching to benefit from caching and optimistic updates
  4. Handle errors gracefully: Always provide user feedback for both success and error states
  5. Use the dashboard's UI components: Maintain visual consistency with the rest of the dashboard
  6. Test in Dev Mode: Use Dev Mode to verify your extensions are placed correctly

What's Next?

Now that you understand the fundamentals of extending the dashboard, explore these specific guides: