Using Vue 2 component in Astro, not Vue3 .... is it possible?
Tags: astro, vue, javascript, webdev
Published: 2023-02-11 10:06:02

## Introduction

Recently, I've become interested in Astro and Island Architecture and have started reading its documentation.

As you know, Astro has a great feature called UI Framework Integrations that allows you to write components using popular frameworks such as React!

![List of UI frameworks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c99x85m2e4y1oz623lqf.png)

According to the official documentation, Astro already supports almost all of the popular frameworks: React, Vue3, and Svelte, which is great...... wait, where is Vue2?

## Vue2 will reach EOL at the end of 2023

Seriously, I know that it has been announced in [the official docs](https://v2.vuejs.org/lts/) that Vue2 will be EOL, so there is no way Astro will support Vue2 in the future. 

However, I am curious if it is technically possible for Astro to support Vue2.

## Let's make Vue2 integration myself!

Fortunately, Astro's documentation already covers [how to create a new integration](https://docs.astro.build/en/reference/integrations-reference/) and is easy to understand. And surprisingly, only three files need to be created to add a new integration: main.js, client.js, and server.js!

Now let's look into how to create a new integration.

## main.js

This file is a sort of configuration file for the integration. When the `astro:config:setup` hook is called, Astro will add a renderer and update Vite settings according to this file.

```js
import { AstroIntegration } from "astro";
import { createVuePlugin } from "vite-plugin-vue2";

function getRenderer() {
  return {
    name: "astro-vue2",
    clientEntrypoint: "astro-vue2/client.js",
    serverEntrypoint: "astro-vue2/server.js",
  };
}

function getViteConfiguration() {
  return {
    optimizeDeps: {
      include: ["astro-vue2/client.js", "vue"],
      exclude: ["astro-vue2/server.js"],
    },
    plugins: [createVuePlugin()],
  };
}

export default function (): AstroIntegration {
  return {
    name: "astro-vue2",
    hooks: {
      "astro:config:setup": ({ addRenderer, updateConfig }) => {
        addRenderer(getRenderer());
        updateConfig({ vite: getViteConfiguration() });
      },
    },
  };
}
```

I have added [vite-plugin-vue2](https://github.com/vitejs/vite-plugin-vue2) to Vite's configuration to allow Vite to build Vue2 components.

And the most remarkable thing is the `getRenderer` function. This function defines two entry points: serverEntrypoint and clientEntrypoint.

The serverEntrypoint is called at build time and SSR (Server Side Rendering). And the clientEntrypoint is called at hydration time.

Now let's delve into the details of these entry points.

## server.js

The serverEntrypoint returns two functions, `check` and `renderToStaticMarkup,` but both tasks are very simple.

```js
import Vue from "vue";
import { SSRLoadedRenderer } from "astro";
import { createRenderer } from "vue-server-renderer";
import { buildScopedSlots } from "./scoped-slots";

const check = async (
  Component,
  _props,
  _children
) => {
  return !!Component["staticRenderFns"];
};

const renderToStaticMarkup =
  async (Component, props, slotted, _metadata) => {
    const instance = new Vue({
      render: (h) =>
        h(Component, { props, scopedSlots: buildScopedSlots(h, slotted) }),
    });
    const html = await createRenderer().renderToString(instance);
    return { html };
  };

export default {
  check,
  renderToStaticMarkup,
};
```

The `check` function simply returns a boolean value indicating whether the given Component is the Vue2 component or not. so I used the `staticRenderFns` property, which only Vue2 has (although I don't know what the property is for...)

Also, The `renderToStaticMarkup` function should return a static HTML string based on the given Component. So I use the [vue-server-renderer](https://github.com/vuejs/vue/tree/dev/packages/vue-server-renderer#readme) library to get the HTML from the Vue2 component.

## client.js

The clientEntrypoint is simpler than the serverEntrypoint! Its role is to hydrate the Vue2 component based on the HTML created at SSR time.

```js
import Vue from "vue";
import { buildScopedSlots } from "./scoped-slots";

Vue.config.ignoredElements.push("astro-slot");

export default (element: any) => {
  return async (
    Component: any,
    props: any,
    slotted: Record<string, string>,
    { client }: { client: string | null }
  ) => {
    new Vue({
      render: (h) =>
        h("astro-island", [
          h(Component, { props, scopedSlots: buildScopedSlots(h, slotted) }),
        ]),
    }).$mount(element, client !== "only");
  };
};
```

Note that Astro has the [client:only](https://docs.astro.build/en/reference/directives-reference/#clientonly) option that allows Astro to behave SPA-like. So I need to pass a boolean as the second argument to `$mount` to tell Vue2 if it should hydrate the component.

## Let's use the Vue2 integration!

All done! Now let's try out this integration! Before trying it, I added the Vue2 integration to Astro's configuration in advance. The `astro-vue2` package is the same as `main.js.`

```js
import { defineConfig } from "astro/config";
import vue2 from "astro-vue2";

// https://astro.build/config
export default defineConfig({
  integrations: [vue2()],
});
```

And I made a very simple Vue2 counter having a `<slot />.`

```vue
<template>
  <div>
    <button v-on:click="handleClick">Vue2 counter: {{ value }}</button>
    <p>slot: <slot /></p>
  </div>
</template>

<script>
export default {
  props: {
    defaultValue: {
      type: Number,
      required: true,
    },
  },
  data() {
    return {
      value: this.defaultValue,
    };
  },
  methods: {
    handleClick() {
      return this.value++;
    },
  },
};
</script>
```

This component is then used in an Astro component. Also, In this astro file, another Astro component is passed to the Vue2 component as a child component.

```js
---
// @ts-ignore
import Counter from "../components/Counter.vue";

import Child from "../components/Child.astro";
import Layout from "../layouts/Layout.astro";
---

<Layout title="Astro and Vue2">
  <main>
    <Counter defaultValue={10} client:visible>
      <Child name={"Astro component"} />
    </Counter>
  </main>
</Layout>
```

Does this actually work properly...? Of course, it does!

![The demo of Vue2 integration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xb65zqpad3sv6v57pxr0.gif)

I've created just three files, but all of Astro's functions work surprisingly fine. Also, you can build these components with the [astro build](https://docs.astro.build/en/reference/cli-reference/#astro-build) command!

## This code is on GitHub

All of the code used in the demo is available on GitHub. If you're interested, you can view the code in this repository.

{% embed https://github.com/ktmouk/astro-vue2 %}

I hope this article will interest you in Astro! 🚀