React vs Vue 3 Comparison
| React | Vue 3 |
|---|---|
useState | ref() / reactive() |
useEffect | onMounted() / watch() |
@tanstack/react-query | @tanstack/vue-query |
react-router-dom | vue-router |
| JSX | SFC Template (.vue) |
children props | <slot /> |
Project Structure
Folder Structure
examples/b2b-admin/web-vue/
├── src/
│ ├── main.ts # App entry point
│ ├── App.vue # Root component
│ ├── api/
│ │ └── client.ts # API client (same as React)
│ ├── types/
│ │ └── api.ts # Type definitions (same as React)
│ ├── composables/ # Vue Hooks (React's hooks/)
│ │ ├── useHealth.ts
│ │ └── useProjects.ts
│ ├── router/
│ │ └── index.ts # Vue Router setup
│ ├── components/
│ │ ├── HealthStatus.vue
│ │ ├── AppLayout.vue
│ │ ├── ProjectForm.vue
│ │ └── ProjectCard.vue
│ └── pages/
│ ├── ProjectsPage.vue
│ └── ProjectDetailPage.vue
├── package.json
├── vite.config.ts
└── tailwind.config.jsComposition API Examples
State Management (ref)
React
React
const [name, setName] = useState('');
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>Vue 3
Vue
const name = ref('');
<input v-model="name" />Vue Query Composable
composables/useProjects.ts
// composables/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import { computed, type Ref } from 'vue';
import { apiClient } from '../api/client';
export function useProjects(page: Ref<number> = ref(1)) {
return useQuery({
queryKey: computed(() => ['projects', page.value]),
queryFn: () =>
apiClient.get(`/api/v1/projects?page=${page.value}`),
});
}
export function useCreateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data) =>
apiClient.post('/api/v1/projects', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
},
});
}Vue Component Example
components/ProjectForm.vue
<script setup lang="ts">
import { ref } from 'vue';
import { useCreateProject } from '../composables/useProjects';
const name = ref('');
const description = ref('');
const { mutate: createProject, isPending } = useCreateProject();
const handleSubmit = () => {
if (!name.value.trim()) return;
createProject(
{ name: name.value.trim(), description: description.value.trim() },
{
onSuccess: () => {
name.value = '';
description.value = '';
},
}
);
};
</script>
<template>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Project Name
</label>
<input
v-model="name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="Enter project name"
required
/>
</div>
<button
type="submit"
:disabled="isPending"
class="w-full bg-blue-600 text-white py-2 rounded-lg"
>
{{ isPending ? 'Creating...' : 'Create Project' }}
</button>
</form>
</template>Running Locally
Terminal
# 1. Start the backend (Hono recommended)
cd examples/b2b-admin/api-hono
npm install
npm run dev
# 2. Start the Vue frontend (new terminal)
cd examples/b2b-admin/web-vue
npm install
npm run dev
# Open http://localhost:5174 in your browserCloudflare Pages Deployment
You can deploy to Cloudflare Pages just like React.
The public/_redirects file is already included,
which resolves SPA routing issues.
| Framework preset | Vite |
| Build command | npm run build |
| Build output directory | dist |
| Root directory | examples/b2b-admin/web-vue |
Key Points
- The API client and type definitions are 100% identical to React
- TanStack Query supports both React and Vue
- The same backend API works regardless of the frontend framework