Vue 3 composables and components for seamless and streamlined interaction with a Medusa.
If you're building a custom vue based storefront that consumes a medusa backend and find yourself wishing you had something nice at hands like medusa-react
to streamline your data management - this might be your library!
The library uses @tanstack/vue-query under the hood.
For the core composables run:
npm install @medusa-vue/core
# or
yarn add @medusa-vue/core
For the components (WIP 👷):
npm install @medusa-vue/components
# or
yarn add @medusa-vue/components
In order to use the composables exposed by this library you need to register it as a plugin in your main file before mounting your app. The plugin takes a config object that needs at least a baseUrl
where it can reach your server. Optionally, it allows you to pass additional props to configure both the underlying medusa-js
and the vue-query
client. Here's the complete interface. Refer to these and these docs, respectively to get an idea on how the parts work together.
interface MedusaVueClientProps {
baseUrl: string;
maxRetries?: number;
/**
* Authentication token
*/
apiKey?: string;
/**
* PublishableApiKey identifier that defines the scope of resources
* available within the request
*/
publishableApiKey?: string;
queryClientProviderProps?: VueQueryPluginOptions;
}
Plug it in:
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { createMedusaVueClient } from '@medusa-vue/core';
const client = createMedusaVueClient({
baseUrl: '<YOUR_SERVER_BASE_URL>',
});
const app = createApp(App);
app.use(client).mount('#app');
Queries simply wrap around vue-query's useQuery
hook to fetch some data from your medusa server
// ./my-product-list.vue
<script setup lang="ts">
import { useProducts } from '@medusa-vue/core';
const { data, error, isLoading } = useProducts();
</script>
<template>
<ul v-for="products in data?.products">
<li>...</li>
</ul>
</template>
Note: If you've worked with @medusajs/medusa-react you might be used to being able to destructure the recordset returned by the server directly, i.e. const { products } = useProducts()
. This is however not possible with vue due to the way it's reactive system works.
Mutations wrap around vue-query's useMutation
to mutate data and perform server-side effects on your medusa server. If you are not entirely familiar with this idea of "mutations", creating a cart would be a mutation because it creates a cart in your server (and database). Mutations also have to be invoked imperatively, meaning that calling for the mutation to take action, you will have to call a mutate()
function returned from mutation hooks.
<script setup lang="ts">
import { useCreateCart } from '@medusa-vue/core';
const createCart = useCreateCart();
const handleClick = () => {
createCart.mutate({}); // create an empty cart
};
</script>
<template>
<Button isLoading="{createCart.isLoading}" onClick="{handleClick}">
Create cart
</Button>
</template>
The mutation hooks will return exactly what vue-query's useMutation
returns. In addition, the options you pass in to the hooks will be passed along to useMutation
.
NOTE: This is still work in progress and new components will gradually be added!:construction_worker:
If you prefer declarative templates, @medusa-vue/components
provided (almost) renderless components to use directly in your template and provide data through slot-props
. This allows for extremely streamlinend and declarative templating:
<script setup lang="ts">
import { UseProducts } from '@medusa-vue/components';
</script>
<template>
<use-products v-slot="{ data, isLoading }">
<loading-spinner v-if="isLoading" />
<product-list :products="data.products" />
</use-products>
</template>
The component also allows to pass down the laoding indicating component via a slot:
<script setup lang="ts">
import { UseProducts } from '@medusa-vue/components';
</script>
<template>
<use-products>
<template #fallback>
<div>Loading....</div>
</template>
<template v-slot="{ data, isLoading }">
<product-list :products="data.products" />
</template>
</use-products>
</template>
A set of utility functions are also exposed from the library to make your life easier when dealing with displaying money amounts
formatVariantPrice(params: FormatVariantPriceParams): string
type FormatVariantPriceParams = {
variant: ProductVariantInfo;
region: RegionInfo;
includeTaxes?: boolean;
minimumFractionDigits?: number;
maximumFractionDigits?: number;
locale?: string;
};
type ProductVariantInfo = Pick<ProductVariant, 'prices'>;
type RegionInfo = {
currency_code: string;
tax_code: string;
tax_rate: number;
};
Given a variant and region, will return a string representing the localized amount (i.e: $19.50
)
The behavior of minimumFractionDigits and maximumFractionDigits is the same as the one explained by MDN here. In fact, in order to convert the decimal amount, we use the browser's Intl.NumberFormat
method.
computeVariantPrice(params: ComputeVariantPriceParams): number
type ComputeVariantPriceParams = {
variant: ProductVariantInfo;
region: RegionInfo;
includeTaxes?: boolean;
};
Determines a variant's price based on the region provided. Returns a decimal number representing the amount.
formatAmount(params: FormatAmountParams): string
type FormatAmountParams = {
amount: number;
region: RegionInfo;
includeTaxes?: boolean;
minimumFractionDigits?: number;
maximumFractionDigits?: number;
locale?: string;
};
Returns a localized string based on the input params representing the amount (i.e: "$10.99").
computeAmount(params: ComputeAmountParams): number
type ComputeAmountParams = {
amount: number;
region: RegionInfo;
includeTaxes?: boolean;
};
Takes an integer amount, a region, and includeTaxes boolean. Returns a decimal amount including (or excluding) taxes.
Based on and inspired by medusa-react. Keep up the good work! 🍻