docs/docs/guides/extending-the-dashboard/tech-stack/index.mdx
The Vendure Dashboard is built on a modern stack of technologies that provide a great developer experience and powerful capabilities for building custom extensions.
The dashboard is built with React 19, giving you access to all the latest React features including:
useOptimistic and useFormStatusimport { useOptimistic, useFormStatus } from 'react';
function OptimisticUpdateExample() {
const [optimisticState, addOptimistic] = useOptimistic(state, (currentState, optimisticValue) => {
// Return new state with optimistic update
return [...currentState, optimisticValue];
});
return (
<div>
{optimisticState.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
Full TypeScript support throughout the dashboard provides:
Vite 7 powers the development experience with:
The dashboard uses Tailwind CSS v4 for styling:
// Example using Tailwind classes
function MyComponent() {
return (
<div className="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">My Custom Component</h2>
</div>
);
}
The Vendure Design System is based on Base UI, Shadcn/ui and Tailwind, providing:
import { Button, Input, Card } from '@vendure/dashboard';
function MyForm() {
return (
<Card className="p-6">
<Input placeholder="Enter your text" />
<Button className="mt-4">Submit</Button>
</Card>
);
}
TanStack Query v5 handles all data fetching and server state management:
import { useQuery } from '@vendure/dashboard';
import { graphql } from '@/gql';
const getProductsQuery = graphql(`
query GetProducts {
products {
items {
id
name
slug
}
}
}
`);
function ProductList() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () => client.request(getProductsQuery),
});
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>
);
}
TanStack Router provides type-safe routing with:
import { Link, useNavigate } from '@vendure/dashboard';
function Navigation() {
const navigate = useNavigate();
return (
<div>
<Link to="/products">Products</Link>
<button onClick={() => navigate({ to: '/customers' })}>Go to Customers</button>
</div>
);
}
React Hook Form provides powerful form handling with:
import { useForm, FormFieldWrapper, Input, Button } from '@vendure/dashboard';
interface FormData {
name: string;
email: string;
}
function MyForm() {
const form = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<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>
);
}
gql.tada provides type-safe GraphQL with:
import { graphql } from '@/gql';
import { useMutation } from '@vendure/dashboard';
const createProductMutation = graphql(`
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
slug
}
}
`);
function CreateProductForm() {
const mutation = useMutation({
mutationFn: (input: CreateProductInput) => client.request(createProductMutation, { input }),
});
// TypeScript knows the exact shape of the input and response
const handleSubmit = (data: CreateProductInput) => {
mutation.mutate(data);
};
return (
// Form implementation
<div>Create Product Form</div>
);
}
Sonner provides toast notifications with:
import { toast } from '@vendure/dashboard';
function MyComponent() {
const handleSave = async () => {
try {
await saveData();
toast.success('Data saved successfully!');
} catch (error) {
toast.error('Failed to save data', {
description: error.message,
});
}
};
// Promise-based toasts
const handleAsyncAction = () => {
toast.promise(performAsyncAction(), {
loading: 'Saving...',
success: 'Saved successfully!',
error: 'Failed to save',
});
};
return (
<div>
<button onClick={handleSave}>Save</button>
<button onClick={handleAsyncAction}>Async Save</button>
</div>
);
}
Lucide React provides beautiful, customizable icons:
import { ShoppingCartIcon, UserIcon, SettingsIcon } from 'lucide-react';
function Navigation() {
return (
<nav className="flex space-x-4">
<a href="/products" className="flex items-center">
<ShoppingCartIcon className="mr-2 h-4 w-4" />
Products
</a>
<a href="/customers" className="flex items-center">
<UserIcon className="mr-2 h-4 w-4" />
Customers
</a>
<a href="/settings" className="flex items-center">
<SettingsIcon className="mr-2 h-4 w-4" />
Settings
</a>
</nav>
);
}
Smooth animations powered by Motion (successor to Framer Motion):
import { motion } from 'motion/react';
function AnimatedCard({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="bg-white p-6 rounded-lg shadow"
>
{children}
</motion.div>
);
}
Lingui provides a powerful i18n solution for React:
import { Trans, useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
return (
<div>
<h1>
<Trans>Welcome to Dashboard</Trans>
</h1>
<p>{t`Click here to continue`}</p>
</div>
);
}