Template to easily use Convex with Vue and Auth0
Everything should be setup to work properly.
Click here to go to the deployed app
This branch adds a bunch of goodies that I like to use for my vue projects and can be fairly opinionated, if you're only interested in the convex stuff, check the minimal branch
- A
createConvex
vue plugin has been created to instanciated a slightly modified version of theConvexReactClient
. - If
auth0
options are provided to the plugin, navigation guards will be added to the application. You can tweak the option to have a different redirect url, or more involved way of determining if an autentication check should be made (by default, addneedsauth: true
to a route block meta). ⚠️ You will need to add your auth0 credentials (domain and client ID)- in a .env.local file under the keys
VITE_AUTH0_DOMAIN
andVITE_AUTH0_CLIENTID
. You can use different names but will have ti make the appropriate changes insrc/main.ts
- as environment variables through the convex dashboard (see here) under the keys
AUTH0_DOMAIN
andAUTH0_APPLICATIONID
. You can use different names but will have ti make the appropriate changes inconvex/auh.config.js
- in a .env.local file under the keys
🧪 - Experimental: might have bugs 🔨 - Available soon
<script setup lang="ts">
import { api } from '@/api';
// queries with no arguments
const messages = useQuery(api.messages.list);
// queries with static arguments
const user = useQuery(api.user.byId, [{ id: 1 }]);
// queries with dynamic arguments: you can use a ref, computed, or getter function
const args = ref(1);
const user = useQuery(api.user.byId, args);
const user = useQuery(
api.user.byId,
computed(() => [{ id: props.userId }])
);
const user = useQuery(api.user.byId, () => [{ id: props.userId }]);
</script>
<template>
<ul v-if="messages">
<li v-for="message in messages" :key="message._id">{{message.text}}</li>
</ul>
</template>
like useQuery but can be awaited and will resolve once the query result is available (either from cache or from a network call). This enables you to use this composable in conjuction with Vue's <Suspense />
. Note: like useQuery, the value will be reactive and will update automatically when it's value changes on the Convex server.
<script setup lang="ts">
import { api } from '@/api';
const messages = await useSuspenseQuery(api.messages.list);
// queries with no arguments
const messages = useSuspenseQuery(api.messages.list);
// queries with static arguments
const user = useSuspenseQuery(api.user.byId, [{ id: 1 }]);
// queries with dynamic arguments: you can use a ref, computed, or getter function
const args = ref(1);
const user = useSuspenseQuery(api.user.byId, args);
const user = useSuspenseQuery(
api.user.byId,
computed(() => [{ id: props.userId }])
);
const user = useSuspenseQuery(api.user.byId, () => [{ id: props.userId }]);
</script>
<template>
<!-- don't forget to add a <Suspense> in the parent component ! -->
<ul>
<li v-for="message in messages" :key="message._id">{{message.text}}</li>
</ul>
</template>
<script setup lang="ts">
import { api } from '@/api';
const ITEMS_PER_PAGE = 5;
const {
results: messages,
status,
loadMore
} = usePaginatedQuery(
api.messages.paginatedList,
{}, // like useQuery and usesuspenseQuery, you can provide a getter function or a reactive value as well
{ initialNumItems: ITEMS_PER_PAGE }
);
</script>
<template>
<ul>
<li v-for="message in messages" :key="message._id">{{message.text}}</li>
</ul>
<button :disabled="status !== 'CanLoadMore'" @click="loadMore(ITEMS_PER_PAGE)">
Load more
</button>
</template>
Now with optimistic updates ! (🧪)
<script setup lang="ts">
import { api } from '@/api';
const { isLoading, mutate: addMessage } = useMutation(api.messages.add, {
optimisticUpdate: (localStore, args) => {
const currentValue = localStore.getQuery(api.messages.list, {});
if (currentValue === undefined) return;
localStore.setQuery(
api.messages.list,
{},
currentValue.concat({
_id: 'optimistic' as Id<'todos'>,
_creationTime: Date.now(),
text: args.text
})
);
}
});
const form = reactive({
text: ''
});
const onSubmit = async () => {
await addMessage(form);
form.text = '';
};
</script>
<template>
<form @submit.prevent="onSubmit">
<label for="text">Write a new message</label>
<input id="text" v-model="form.text" />
<button :disabled="isLoading">Add todo</button>
</form>
</template>
<script setup lang="ts">
import { api } from '@/api';
const { isLoading, execute } = useAction(api.some.action);
</script>
<template>
<button :disables="isLoading" @click="execute()">Do the action thingie</button>
</template>
if you need to use the ConvexVueClient directly. You probably don't need it.
if you used the auth0
option in the plugin, it will return you the loading and authenticated state. For additional auth utilities like login, logout, user etc, please use useAuth0
from @auth0/auth0-vue
<script setup lang="html">
const { isLoading, isAuthenticated } = useConvexAuth()
</script>
Allows you to display content depending on convex Auth status.
- default: when the user is logged in
- fallback: when the user isn't logged in
- loading: when the authentication process is pending
<script setup lang="ts">
const { loginWithRedirect } = useAuth0();
</script>
<template>
<EnsureAuthenticated>
<template #loading>Authenticating...</template>
<template #fallback>
You need to be logged in to see this content
<button @click="loginWithRedirect()">Login</button>
</template>
<template #default="{ user }">Welcome back, {{ user.nickname }} !</template>
</EnsureAuthenticated>
</template>
Allows you to display different UI during the lifecycle of a convex query. It takes the following props
query
: a function taking the convex api as a parameter and returning a query, eg::query="api => api.messages.list"
args
: the arguments for the returned query
It accepts the following slots:
- default: should be used to display the query results (avaiable via scoped slots)
- loading: pending state when the query is loading for the first time
- error: should be used to displau an error UI. It takes the error as slot props, as well as a
clearError
function to clear the underlying error b boundary (note: doing so will retry the query).
It will also emit the error when / if it happens.
<script setup lang="ts">
const handleError = (err: Error) => sendErrorToAnalytics(err);
</script>
<Query :query="api => api.messages.list" :arg="{ sentBy: 'Bob'}" @error="handleError">
<template #loading>Loading messages...</template>
<template #error="{ error, clearError}">
An error has occured
<pre>{{ error }}</pre>
<button @click="clearError">Retry</button>
</template>
<template #default="{ data: messages }">
<ChatWidget :messages="messages">
</template>
</Query>
Similar to <Query />
, but handles pagination
<script setup lang="ts">
const ITEMS_PER_PAGE = 5;
</script>
<template>
<PaginatedQuery
v-slot="{ data: todos, status, loadMore }"
:num-items="ITEMS_PER_PAGE"
:query="api => api.messages.paginatedList"
:args="{}"
>
<template #loading>Loading messages...</template>
<template #error="{ error, clearError}">
An error has occured
<pre>{{ error }}</pre>
<button @click="clearError">Retry</button>
</template>
<template #default="{ data: messages, status, loadMore }">
<p v-if="!messages.length">No todos yet !</p>
<ul>
<li v-for="message in messages" :key="message._id">{{message.text}}</li>
</ul>
<UiButton :disabled="status !== 'CanLoadMore'" @click="loadMore(ITEMS_PER_PAGE)">
Load more
</UiButton>
</template>
</PaginatedQuery>
</template>
This branch adds a lot of other stuff for better DX and is overall more batteries included. It's fairly opinionated so if you just want to use convex with vue check out the minimal
branch
- Auto import for vue, @vueuse/core, vee-validate, vue-router, as well as the
composables
andutils
directories - Auto imported vue components in the
components
directory, as well asall components of the Ark-ui library under theArk
prefix, eg.ArkAccordion
- Open-props, as well as UnoCSS for styling. A preset a custom UNO theme has been added to use open-props' values. The uno config also scans your
src/styles/theme.css
file to add additional colors, see the file for more informations - vee-validate for form management
- A few UI components for buttons, inputs etc...nothing fancy
- Iconify component (UiIcon) to easily add an icon from inconify. This can be used instead of uno's
presetIcons
when you want to be able to pass an icon name as props for example. - PWA Support.
⚠️ You...might wanna make sure you replace the icons inèpublic/icons, as I wil be forever 15 years old. - A few utility types have been added in
src/utils/types
- A theme supporting dark mode, and a dark mode component