Building a multi-framework dashboard with Astro
Tags: astro, javascript, webdev
Published: 2023-02-07 15:45:15

**Written by [Emanuel Suriano](https://blog.logrocket.com/author/emanuelsuriano/)✏️**

In this article, we will be exploring the process of building a website in [Astro](https://astro.build/) using components from three of the most popular frameworks: React, Svelte, and Vue.js. 

Each of these frameworks has its strengths and weaknesses, and in this article, we will provide a step-by-step guide to building a website using each of them together. This means that I can take the best of each community and put it into the project! 

_Jump ahead:_

*   [Project characteristics](#project-characteristics)
*   [Why would you want to use multiple frameworks in one project?](#why-use-multiple-frameworks-one-project)
*   [Setting up a fresh Astro project](#setting-up-astro-project)
*   [Adding the UI frameworks](#adding-ui-frameworks)
*   [Setting up our layout](#setting-up-layout)
*   [Fetching the dashboard data](#fetching-dashboard-data)
*   [Integrating Svelte components](#integrating-svelte-components)
    *   [Integrating a `Table` component](#integrating-table-component)
    *   [Integrating Svelte category tags](#integrating-svelte-category-tags)
*   [Data synchronization and state management with nanostores](#data-synchronization-state-management-nanostores)
*   [Vue integration: Overview](#vue-integration-overview)
*   [React integration: `CategoryChart`](#react-integration-categorychart)
*   [Making the app responsive](#making-app-responsive)

## Project characteristics

For this demo, we are going to use the following libraries to build the dashboard:

*   React: [MUI Core](https://mui.com/core/) and [recharts](https://recharts.org/)
*   Svelte: [Svelte Material UI](https://sveltematerialui.com/)
*   Vue: [Vuetify](https://vuetifyjs.com/en/)

I also installed [Tailwind CSS](https://tailwindcss.com/) to create most of the layout for the page, with the prefix option of `tw-` (this is to prevent clashing classes between [Tailwind and Vuetify](https://github.com/tailwindlabs/tailwindcss/issues/465#issuecomment-388496395)). 

Our end goal is to build the following dashboard: ![Our final multi-framework dashboard](https://blog.logrocket.com/wp-content/uploads/2023/01/final-dashboard-preview.png) 

Something important to highlight is that all of our components are connected, and an interaction in one can trigger a change in the other. This is possible by using `nanostores` (we are going to cover it later in the article). ![Components are connected: interacting with the React category chart displays changes in the Svelte visual diagram](https://blog.logrocket.com/wp-content/uploads/2023/01/component-interaction-dashboard.gif) Components are connected: interacting with the Svelte category chart displays changes in the React visual diagram

Finally, for demo purposes, I built a framework highlighter to visually display the UI framework (React, Vue, or Svelte) responsible for rendering a component. This function can be activated by clicking the blue **eye icon** at the bottom of the page. ![The UI framework highlighter indicates which components were built with which framework](https://blog.logrocket.com/wp-content/uploads/2023/01/ui-framework-highlighter.png) 

You can also check out the deployed application [here](https://astro-multi-framework-dashboard.netlify.app/).

## Why would you want to use multiple frameworks in one project? <a name="why-use-multiple-frameworks-one-project">

Building a multi-framework application using Astro can be a powerful and efficient way to create complex and versatile software. By combining the strengths of different frameworks, developers can create a more robust and flexible application that can adapt to changing requirements and meet the diverse needs of users. Using Astro, developers can easily integrate multiple frameworks, allowing them to take advantage of the best features of each one and create a truly exceptional application. 

In addition to its ability to combine multiple frameworks, Astro is also great for migrations. As software systems and requirements evolve, it may become necessary to migrate to a new framework or set of frameworks. With Astro, this process can be streamlined and made more efficient, allowing developers to quickly and easily move their applications to new frameworks without significant downtime or disruptions. This can save time, effort, and resources, making Astro a valuable tool for anyone looking to migrate their application to new frameworks.

## Setting up a fresh Astro project <a name="setting-up-astro-project">

The easiest way to create a [new project with Astro](https://docs.astro.build/en/getting-started/) is by using their CLI, `create-astro`, which comes with maybe one of the friendliest coding assistants ever: Houston!

```bash
# create a new project with npm
> yarn create astro

╭─────╮  Houston:
│ ◠ ◡ ◠  Let's make the web weird!
╰─────╯

 astro   v1.6.15 Launch sequence initiated.

✔ Where would you like to create your new project? … astro-dashboard
✔ How would you like to setup your new project? › a few best practices (recommended)
✔ Template copied!
✔ Would you like to install yarn dependencies? (recommended) … yes
✔ Packages installed!
✔ Would you like to initialize a new git repository? (optional) … yes
✔ Git repository created!
✔ How would you like to setup TypeScript? › Strict
✔ TypeScript settings applied!

  next   Liftoff confirmed. Explore your project!

         Enter your project directory using cd ./astro-dashboard
         Run yarn dev to start the dev server. CTRL+C to stop.
         Add frameworks like react or tailwind using astro add.

         Stuck? Join us at https://astro.build/chat

╭─────╮  Houston:
│ ◠ ◡ ◠  Good luck out there, astronaut!
╰─────╯
```

After that, you should end up with a folder that has a structure similar to this:

```plaintext
/astro-dashboard
├── README.md
├── astro.config.mjs
├── output.txt
├── package.json
├── public
|  └── favicon.svg
├── node_modules # sub folders ignored to keep the directory tree short
├── src
|  ├── components
|  |  └── Card.astro
|  ├── env.d.ts
|  ├── layouts
|  |  └── Layout.astro
|  └── pages
|     └── index.astro
├── tsconfig.json
└── yarn.lock
```

Let's start our development server by running:

```bash
> yarn dev

yarn run v1.22.19
$ astro dev
  🚀  astro  v1.6.15 started in 57ms

  ┃ Local    http://127.0.0.1:3000/
  ┃ Network  use --host to expose
```

After opening the `localhost` URL, you should see the following screen in your browser: ![The Astro start page](https://blog.logrocket.com/wp-content/uploads/2023/01/astro-start-page.png)

## Adding the UI frameworks <a name="adding-ui-frameworks">

The official Astro documentation has a summary of all their [official integrations](https://docs.astro.build/en/guides/integrations-guide/#official-integrations), covering the topics of UI frameworks, SSR adapters, and others, such as Tailwind. 

The Astro team made it simple for everybody and includes the command `astro add` in all new projects, which we can use to install any of the official integrations. We simply need to execute:

```bash
> yarn astro add react vue svelte tailwind

✔ Resolving packages...

  Astro will run the following command:
  If you skip this step, you can always run it yourself later

╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ yarn add @astrojs/react @astrojs/svelte @astrojs/tailwind @astrojs/vue @types/react-dom@^18.0.6     │
│ @types/react@^18.0.21 react-dom@^18.0.0 react@^18.0.0 svelte@^3.46.4 tailwindcss@^3.0.24            │
│ vue@^3.2.30                                                                                         │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯

✔ Continue? … yes
✔ Installing dependencies...

  Astro will generate a minimal ./tailwind.config.cjs file.

✔ Continue? … yes

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ───────────────────────────────────────╮
 │ import { defineConfig } from 'astro/config';            │
 │                                                         │
 │ // https://astro.build/config                           │
 │ import react from "@astrojs/react";                     │
 │ import vue from "@astrojs/vue";                         │
 │ import svelte from "@astrojs/svelte";                   │
 │ import tailwind from "@astrojs/tailwind";               │
 │                                                         │
 │ // https://astro.build/config                           │
 │ export default defineConfig({                           │
 │   integrations: [react(), vue(), svelte(), tailwind()]  │
 │ });                                                     │
 ╰─────────────────────────────────────────────────────────╯

✔ Continue? … yes

   success  Added the following integrations to your project:
  - @astrojs/react
  - @astrojs/vue
  - @astrojs/svelte
  - @astrojs/tailwind

  Astro will make the following changes to your tsconfig.json:

 ╭ tsconfig.json ──────────────────────────╮
 │ {                                       │
 │   "extends": "astro/tsconfigs/strict",  │
 │   "compilerOptions": {                  │
 │     "jsx": "react-jsx",                 │
 │     "jsxImportSource": "react"          │
 │   }                                     │
 │ }                                       │
 ╰─────────────────────────────────────────╯

✔ Continue? … yes

   success  Successfully updated TypeScript settings
```

As you can see, Astro took over all the manual tasks of installing the integrations, adding them to our Astro configuration file, and even updating our `tsconfig.json` to make React work nicely with Typescript. Great job, Houston!

## Setting up our layout <a name="setting-up-layout">

Let's start by setting up our `Layout` for the page, which consists of a `Header`, a `Footer`, and a container for our page content. To avoid several changes when we introduce `vuetify`, it’s better to change the `prefix` inside our `tailwind.config.cjs` file, like so:

```diff
# tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  theme: {
    extend: {},
  },
  plugins: [],
+  prefix: 'tw-',
};
```

Let's apply the modification to our `Layout`:

```javascript
// src/layouts/Layout.astro
---
type Props = {
  title: string;
};

const { title } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <title>{title}</title>
  </head>
  <body>
    <div class="tw-flex tw-h-screen tw-overflow-hidden">
      <main
        class="tw-relative tw-bg-white tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto tw-overflow-x-hidden"
      >
        <!-- Header -->
        <nav class="tw-bg-white tw-shadow-sm tw-px-2 sm:tw-px-4 tw-py-2.5">
          <div
            class="container tw-flex tw-flex-wrap tw-justify-between tw-items-center tw-mx-auto"
          >
            <a href="/" class="tw-flex tw-items-center">
              <img
                src="/favicon.svg"
                class="tw-mr-3 tw-h-6 sm:tw-h-9"
                alt="Astro Logo"
              />
              <span
                class="tw-self-center tw-text-xl tw-font-semibold tw-whitespace-nowrap"
                >{title}
              </span>
            </a>
          </div>
        </nav>

        <!-- Content container -->
        <div class="tw-container tw-mx-auto tw-my-10 tw-flex-grow">
          <slot />
        </div>

        <!-- Footer -->
        <footer
          class="tw-p-4 tw-bg-white tw-rounded-lg tw-shadow md:tw-flex md:tw-items-center md:tw-justify-between md:tw-p-6"
        >
          <span class="tw-text-sm tw-text-gray-500 sm:tw-text-center">
            Built with
            <a href="https://astro.build/" class="hover:tw-underline">Astro</a>
          </span>
        </footer>
      </main>
    </div>
  </body>
</html>
```

Without making any changes to our initial page (`src/pages/index.astro`) and applying these changes, you should see the following screen: ![Our final layout setup in Astro](https://blog.logrocket.com/wp-content/uploads/2023/01/final-layout-setup.png)

## Fetching the dashboard data <a name="fetching-dashboard-data">

For this demo, I decided to use the [Products list endpoint](https://dummyjson.com/docs/products) from [`https://dummyjson.com/`](https://dummyjson.com/), which provides several public REST APIs that we can use to simulate getting data from our servers. When we call the API, the output has the following structure:

```json
{
  "products": [
    {
      "id": 1,
      "title": "iPhone 9",
      "description": "An apple mobile which is nothing like apple",
      "price": 549,
      "discountPercentage": 12.96,
      "rating": 4.69,
      "stock": 94,
      "brand": "Apple",
      "category": "smartphones",
      "thumbnail": "...",
      "images": ["...", "...", "..."]
    },
    {...},
    {...},
    {...}
    // 30 items
  ],

  "total": 100,
  "skip": 0,
  "limit": 30
}
```

Given that we are working on a TypeScript project, it's always recommended to type our API responses. For that, we can use the following type for this response:

```typescript
// src/types.ts
export type Product = {
  id: number;
  title: string;
  description: string;
  price: number;
  discountPercentage: number;
  rating: number;
  stock: number;
  brand: string;
  category: string;
  thumbnail: string;
};
```

To fetch data using Astro, we can use the `fetch` function to call the endpoint before rendering our component, with the added benefit that we don't need to handle loading states in our application. This way of loading data is similar to how data is loaded on static sites, where the endpoints are called on at build time. 

At the same time, Astro also supports [server-side rendering](https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project). It's always important to understand what kind of application is better to implement given your needs. 

Since our data source can be considered static — the list of products doesn't change over time — running our queries is the best fit to improve the performance and UX of the application. Let's make the following modification to our initial page:

```diff
// src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
+ import type { Product } from '../types';

// fetch data
+ const products: Product[] = await fetch('https://dummyjson.com/products')
+   .then((res) => res.json())
+   .then((res) => res.products);
---

<Layout title="Welcome to Astro.">
  <main>
    <h1>Welcome to <span class="text-gradient">Astro</span></h1>
...
```

## Integrating Svelte components in an Astro app <a name="integrating-svelte-components">

Time to render our data! Let’s revisit the goal of this article: we want to use multiple UI frameworks in the same project. Remember, we already installed all the UI integrations into the project, so the only part missing is adding our components. Let's go one by one, starting with Svelte!

### Integrating a Table component <a name="integrating-table-component">

As I mentioned at the beginning of this article, we’re using [Svelte Material UI](https://sveltematerialui.com/). To install it in our project, we have to first add it to our dependencies:

```bash
> yarn add svelte-material-ui
```

After that, we have to import the necessary CSS file for the components. The best place for those is in our `Layout` component because it will be used inside every screen in our application.

```diff
// src/layouts/Layout.astro
---
+ import 'svelte-material-ui/bare.css';

type Props = {
  title: string;
};

const { title } = Astro.props;
---

<!DOCTYPE html>
...
```

When dealing with npm dependencies in Astro, which is built on top of [Vite](https://vitejs.dev/), that are not previously compiled (converted into JavaScript), we have to specify Vite to the property `ssr.noExternal` in our Astro configuration, inside the `vite` attribute. If you want to read more about Externals, please refer to the [official documentation](https://vitejs.dev/guide/ssr.html#ssr-externals).

```diff
// astro.config.mjs
import { defineConfig } from 'astro/config';

// https://astro.build/config
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
import tailwind from '@astrojs/tailwind';

// https://astro.build/config
export default defineConfig({
  integrations: [react(), vue(), svelte(), tailwind()],
+  vite: {
+    ssr: {
+      noExternal: ['@smui/**/*'],
+    },
+  },
});
```

Next, let's build our first Svelte component to show a list of products inside a table, which is going to be called `ProductTable`. Luckily, `smui` provides a `Table` component called [`Data Table`](https://sveltematerialui.com/demo/data-table/). Below is the full code of the component:

```javascript
// src/components/ProductTable.svelte
<script lang="ts">
  import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
  import type { Product } from 'types';

  export let products: Product[];
</script>

<DataTable style="width: 100%;">
  <Head>
    <Row>
      <Cell style="width: 100%;">Title</Cell>
      <Cell>Category</Cell>
      <Cell>Description</Cell>
      <Cell>Price</Cell>
      <Cell>Stock</Cell>
    </Row>
  </Head>
  <Body>
    {#each products as item (item.id)}
      <Row>
        <Cell>{item.title}</Cell>
        <Cell>{item.category}</Cell>
        <Cell>{item.description}</Cell>
        <Cell numeric>{item.price}</Cell>
        <Cell numeric>{item.stock}</Cell>
      </Row>
    {/each}
  </Body>
</DataTable>
```

Now, let's render it inside our main page. I got rid of the `Card` components that were rendered in the template project.

```javascript
// src/page/index.astro
---
import Layout from '../layouts/Layout.astro';
import ProductTable from '../components/ProductTable.svelte';

import type { Product } from '../types';

// fetch data
const products: Product[] = await fetch('https://dummyjson.com/products')
  .then((res) => res.json())
  .then((res) => res.products);
---

<Layout title="Welcome to Astro.">
  <ProductTable products={products} />
</Layout>
```

Our application should look like this: ![Our integrated Svelte table component](https://blog.logrocket.com/wp-content/uploads/2023/01/integrated-svelte-table-component.png)

### Integrating Svelte category tags <a name="integrating-svelte-category-tags">

One key feature of this dashboard is the ability to see all the existing categories and mark them as visible or hidden. To build this component, we are going to use the component [Chips](https://sveltematerialui.com/demo/chips/) from `smui`. This is a possible implementation of the component:

```javascript
// src/component/CategoryTags.astro
<script lang="ts">
  import Chip, { Set, Text } from '@smui/chips';
  import type { Product } from '../types';

  export let products: Product[];

  let categories = products
    .map((product) => product.category)
    .filter((val, i, arr) => arr.indexOf(val) === i);

  let selected = Array.from(categories);
</script>

<h1>Categories</h1>

<Set chips={categories} let:chip filter bind:selected>
  <Chip {chip} touch>
    <Text>{chip}</Text>
  </Chip>
</Set>
```

Given that this component is interactive, we need to specify a [client directive](https://docs.astro.build/en/reference/directives-reference/#client-directives) from Astro to load the JavaScript alongside it — otherwise, nothing will happen when clicking the categories. Let's add the component to our main page with the `client:load` directive.

```diff
// src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import ProductTable from '../components/ProductTable.svelte';
+ import CategoryTags from '../components/CategoryTags.svelte';

import type { Product } from '../types';

// fetch data
const products: Product[] = await fetch('https://dummyjson.com/products')
  .then((res) => res.json())
  .then((res) => res.products);
---

<Layout title="Welcome to Astro.">
+  <CategoryTags products={products} client:load />
  <ProductTable products={products} client:load />
</Layout>
```

The application is looking now quite similar to our dashboard! ![Svelte category tags are integrated into our dashboard](https://blog.logrocket.com/wp-content/uploads/2023/01/integrated-svelte-category-tags.png)

## Data synchronization and state management with `nanostores` <a name="data-synchronization-state-management-nanostores">

Have you spotted any bugs in the previous screen? The categories that are supposed to be hidden are still visible in our table. The reason for that is that we aren’t communicating data between our `ProductTable` and the `CategoryTags`. Time to talk about state management! 

How do we share information between my components, given that each library has different ways of achieving that? We have a few different options:

*   React has the [Context API](https://reactjs.org/docs/context.html), with a `Provider` / `Consumer` pattern
*   Vue provides a [reactive API](https://vuejs.org/guide/scaling-up/state-management.html#simple-state-management-with-reactivity-api), following the conventions of `Actions --> State --> View` in a similar style to Redux
*   Svelte recommends using [Stores](https://svelte.dev/docs#run-time-svelte-store), providing `readable`, `writable`, and `derived` containers for storing data

All of these are valid ways of fixing the state management issue, but it's clear that we need to find a common solution that works for all the UI libraries. This is where [`nanostores`](https://github.com/nanostores/nanostores) comes into play! The description they provide on their GitHub page is simply perfect: 

A tiny state manager for React, React Native, Preact, Vue, Svelte, and vanilla JS. It uses many atomic stores and direct manipulation. 

The API is simple as:

```javascript
import { atom } from 'nanostores';

// definition of state for primitive values
export const counter = atom(0);

// read and write state
counter.set(counter.get() + 1);

// definition of state for objects
export const car = map({
  brand: 'Tesla',
  km: 0,
});

// update property inside object
car.setKey('km', car.get().km + 1000);
```

nanostores provides many APIs to have more control over state updates and notifications, so I highly recommend checking their documentation in case you want to dig deeper. 

Nevertheless, an important part to cover is the integration with the different UI libraries. For each framework, they provide a separate package that will follows the package’s coding standard:

*   React ([`@nanostores/react`](https://github.com/nanostores/react)): provides a `useStore()` Hook to get the store’s value and re-render the component on store’s changes
*   Vue ([`@nanostores/vue`](https://github.com/nanostores/vue)): provides a `useStore()` composable function to get the store’s value and re-render the component on store’s changes
*   Svelte: `nanostores` implements [Svelte's store contract](https://svelte.dev/docs#component-format-script-4-prefix-stores-with-$-to-access-their-values-store-contract); we just need to put `$` before the store variable to get the value and subscribe for changes

To add `nanostores` into our project, we just need to install the main `nanostores` dependency and the integrations that we need (in our case, one each for `vue` and `react`).

```bash
> yarn add nanostores @nanostores/react @nanostores/vue
```

It's also recommended to store our _store_ in a shared location where all the components can access it. For our categories scenario, we need a simple array of `string`s where we can add or remove the categories to hide them.

```typescript
// src/state.ts
import { atom } from 'nanostores';

export const hiddenCategories = atom<string[]>([]);
```

Let's connect our two "isolated" components. The `CategoryTags` component has to be able to read and write from `hiddenCategories`.

```diff
// src/components/CategoryTags.svelte
<script lang="ts">
  import Chip, { Set, Text } from '@smui/chips';
  import type { Product } from '../types';
+  import { hiddenCategories } from '../state';

  export let products: Product[];

  let categories = products
    .map((product) => product.category)
    .filter((val, i, arr) => arr.indexOf(val) === i);

  let selected = Array.from(categories);

+  function updateSelection() {
+    hiddenCategories.set(
+      categories.filter((category) => !selected.includes(category)),
+    );
+  }
</script>

<h1>Categories</h1>

<Set
  chips={categories}
  let:chip
  filter
  bind:selected
+  on:click={updateSelection}
>
  <Chip {chip} touch>
    <Text>{chip}</Text>
  </Chip>
</Set>
```

Our `ProductTable` has only to read from `hiddenCategories`, and with that, filter out the product that belongs to the categories in our state.

```diff
// src/components/ProductTable.svelte
<script lang="ts">
  import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
  import type { Product } from '../types';
+  import { hiddenCategories } from '../state';

  export let products: Product[];
</script>

<DataTable style="width: 100%;">
  <Head>
    <Row>
      <Cell style="width: 100%;">Title</Cell>
      <Cell>Category</Cell>
      <Cell>Description</Cell>
      <Cell>Price</Cell>
      <Cell>Stock</Cell>
    </Row>
  </Head>
  <Body>
+    {#each products.filter((product) => !$hiddenCategories.includes(product.category)) as item (item.id)}
      <Row>
        <Cell>{item.title}</Cell>
        <Cell>{item.category}</Cell>
        <Cell>{item.description}</Cell>
        <Cell numeric>{item.price}</Cell>
        <Cell numeric>{item.stock}</Cell>
      </Row>
    {/each}
  </Body>
</DataTable>
```

After removing the `Smartphones` and `Laptops` categories from the `CategoryTags`, we can see they don't exist any more in the `ProductTable`: ![Smartphones and laptops are removed from our categories](https://blog.logrocket.com/wp-content/uploads/2023/01/smartphones-laptops-removed-categories.png)

## Vue integration: Overview <a name="vue-integration-overview">

For Vue, I decided to use the most popular component framework option in the ecosystem: [Vuetify](https://next.vuetifyjs.com/en/). The installation can be a bit tricky for alternative projects (like this one!), so I hope to make it as clear as possible. Let's start by installing the necessary dependencies (be aware that only the latest version, v3, is compatible with Vite):

```bash
> yarn add vuetify@^3.0.5
```

Similar to what we did for `smui`, we need to import the component library styles inside our `Layout`:

```diff
---
import 'svelte-material-ui/bare.css';
+ import 'vuetify/styles';

type Props = {
  title: string;
};

const { title } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
  <head>
...
```

Next, we need to hook `vuetify` into our `vue` application. This was impossible in Astro just a few versions ago, until the introduction of the `_app` file inside the `pages` folder, which grants access to the `App` instance of `vue`.

```typescript
//src/pages/_app.ts
import type { App } from 'vue';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';

export default (app: App) => {
  const vuetify = createVuetify({
    ssr: true,
    components,
    directives,
  });

  app.use(vuetify);
};
```

A couple of modifications in our `astro.config.mjs` file: We need to mark the dependency as `noExternal` and link the Astro Vue integration with the entry point that we've just defined.

```diff
// astro.config.mjs
import { defineConfig } from 'astro/config';

// https://astro.build/config
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
import tailwind from '@astrojs/tailwind';

// https://astro.build/config
export default defineConfig({
  integrations: [
    react(),
+    vue({ appEntrypoint: '/src/pages/_app' }),
    svelte(),
    tailwind(),
  ],
  vite: {
    ssr: {
+      noExternal: ['@smui/**/*', 'vuetify'],
    },
  },
});
```

Now, we can implement our `Summary` component, which is going to give us a glance at the **Total Stock** and **Total brands** categories and the selected products inside them. This sounds perfect for the [`v-card component`](https://next.vuetifyjs.com/en/components/cards/)!

```typescript
// src/components/Overview.vue
<script setup lang="ts">
import type { Product } from 'types';
import { hiddenCategories } from '../state';
import { useStore } from '@nanostores/vue';

type Props = {
  products: Product[];
};

const { products } = defineProps<Props>();
const $hiddenCategories = useStore(hiddenCategories);
</script>

<template>
  <h1>Overview</h1>

  <v-container>
    <v-row dense>
      <v-col cols="12">
        <v-card>
          <v-card-text>
            <div>Total stock</div>
            <p class="text-h4 text--primary">
              <!-- Calculating stock with the value from store -->
              {{
                products
                  .filter(
                    (product) => !$hiddenCategories.includes(product.category),
                  )
                  .reduce((acc, curr) => acc + curr.stock, 0)
              }}
            </p>
          </v-card-text>
        </v-card>
      </v-col>

      <v-col cols="12">
        <v-card>
          <v-card-text>
            <div>Total brands</div>
            <p class="text-h4 text--primary">
              <!-- Calculating brand amount with the value from store -->
              {{
                products
                  .filter(
                    (product) => !$hiddenCategories.includes(product.category),
                  )
                  .filter((product, i, array) => array.indexOf(product) === i)
                  .length
              }}
            </p>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>
```

Don't forget to add it to the main page as well!

```diff
// src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import type { Product } from '../types';

import ProductTable from '../components/ProductTable.svelte';
import CategoryTags from '../components/CategoryTags.svelte';
import Overview from '../components/Overview.vue';

// fetch data
const products: Product[] = await fetch('https://dummyjson.com/products')
  .then((res) => res.json())
  .then((res) => res.products);
---

<Layout title="Welcome to Astro.">
  <CategoryTags products={products} client:load />
+ <Overview products={products} client:load />
  <ProductTable products={products} client:load />
</Layout>
```

Here’s what our dashboard looks like now: ![Our summary of total brands and total stock is added to the dashboard](https://blog.logrocket.com/wp-content/uploads/2023/01/total-stock-total-brands-summary.png)

## React integration: `CategoryChart` <a name="react-integration-categorychart">

This is the last integration of this article, and maybe the most fun! A dashboard cannot be considered complete unless we show a chart or diagram to visually display information. For this part of the demo, we’ll use [`mui`](https://mui.com/) for the components and [`recharts`](https://recharts.org/) for the graphs. 

The installation is quite straightforward, and maybe the easiest in the whole tutorial:

```bash
# installation for mui
> yarn add @mui/material @emotion/react @emotion/styled

# installation for recharts
> yarn add recharts
```

We want to allow the user to visually see the `stock` amount and `rating` for the visible category, so they can understand how is the proportion balanced. This is the perfect scenario for a [`RadialBarChart`](https://recharts.org/en-US/examples/SimpleRadialBarChart) in `recharts`. Then, to toggle which value to visualize (either stocks or rating) we can use [`ToggleButton`](https://mui.com/material-ui/react-toggle-button/) from `mui`. This is a possible implementation of it:

```typescript
// src/components/CategoryChart.tsx
import { useStore } from '@nanostores/react';
import React, { useMemo } from 'react';
import {
  RadialBarChart,
  RadialBar,
  Legend,
  ResponsiveContainer,
} from 'recharts';
import type { Product } from '../types';
import { hiddenCategories } from '../state';
import randomColor from 'randomcolor';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';

const style = {
  top: '50%',
  right: 0,
  transform: 'translate(0, -50%)',
  lineHeight: '30px',
};

type Props = {
  products: Product[];
};

type View = 'stock' | 'rating';

const groupByCategory = (products: Product[]) => {
  return products.reduce((acc, curr) => {
    const existingCategory = acc[curr.category];

    acc[curr.category] = existingCategory
      ? existingCategory.concat(curr)
      : [curr];
    return acc;
  }, {} as Record<string, Product[]>);
};

const CategoryChart = ({ products }: Props) => {
  const [view, setView] = React.useState<View>('stock');
  const disabledCategories = useStore(hiddenCategories);

  const data = useMemo(() => {
    const visibleCategories = products
      .map((product) => product.category)
      .filter((product) => !disabledCategories.includes(product))
      .filter((val, i, arr) => arr.indexOf(val) === i);

    const groupedByCategory = groupByCategory(products);

    return visibleCategories.map((category) => {
      const productsInCategory = groupedByCategory[category];
      const avg =
        productsInCategory.reduce((acc, curr) => acc + curr.rating, 0) /
        products.length;

      return {
        name: category,
        stock: productsInCategory.reduce((acc, curr) => acc + curr.stock, 0),
        rating: Math.round(avg * 100) / 100,
        fill: randomColor({ seed: category, luminosity: 'dark' }),
      };
    });
  }, [products, disabledCategories]);

  return (
    <div style={{ width: '100%', height: 400, position: 'relative' }}>
      <ResponsiveContainer width="100%" height="100%">
        <RadialBarChart
          cx="50%"
          cy="50%"
          innerRadius="20%"
          outerRadius="80%"
          barSize={15}
          data={data}
        >
          <RadialBar
            minAngle={15}
            label={{ position: 'insideStart', fill: '#fff' }}
            background
            dataKey={view}
          />

          <Legend
            iconSize={20}
            layout="vertical"
            verticalAlign="middle"
            wrapperStyle={style}
          />
        </RadialBarChart>
      </ResponsiveContainer>

      <ToggleButtonGroup
        style={{ position: 'absolute', top: 0 }}
        color="primary"
        value={view}
        exclusive
        onChange={(_, val) => val && setView(val)}
      >
        <ToggleButton value="stock">Stock</ToggleButton>
        <ToggleButton value="rating">Rating</ToggleButton>
      </ToggleButtonGroup>
    </div>
  );
};

export default CategoryChart;
```

Let's add it to our page and check how it integrates into our dashboard!

```diff
// src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import type { Product } from '../types';

import ProductTable from '../components/ProductTable.svelte';
import CategoryTags from '../components/CategoryTags.svelte';
import Overview from '../components/Overview.vue';
import CategoryChart from '../components/CategoryChart';

// fetch data
const products: Product[] = await fetch('https://dummyjson.com/products')
  .then((res) => res.json())
  .then((res) => res.products);
---

<Layout title="Welcome to Astro.">
  <CategoryTags products={products} client:load />
+  <Overview products={products} client:load />
  <CategoryChart products={products} client:load />
  <ProductTable products={products} client:load />
</Layout>
```

![Stock and rating are now visible for each category](https://blog.logrocket.com/wp-content/uploads/2023/01/stock-rating-visible-category.png)

## Making the app responsive <a name="making-app-responsive">

Now that we have all our components fully coded and integrated, we need to take care of the application’s responsiveness. This is where Tailwind can make our life easier, and in a couple of lines, we have fantastic results!

```dish
//src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import type { Product } from '../types';

import ProductTable from '../components/ProductTable.svelte';
import CategoryTags from '../components/CategoryTags.svelte';
import Overview from '../components/Overview.vue';
import CategoryChart from '../components/CategoryChart';

// fetch data
const products: Product[] = await fetch('https://dummyjson.com/products')
  .then((res) => res.json())
  .then((res) => res.products);
---

<Layout title="Welcome to Astro.">
+  <div class="tw-grid tw-gap-4 tw-grid-cols-3 lg:tw-grid-cols-5 md:tw-gap-10">
+    <div class="tw-col-span-1">
      <CategoryTags client:load products={products} />
+    </div>

+    <div class="tw-col-span-1">
      <Overview client:load products={products} />
+    </div>

+    <div class="tw-col-span-3">
      <CategoryChart client:only="react" products={products} />
+    </div>

+    <div class="tw-col-span-full">
      <ProductTable client:load products={products} />
+    </div>
  </div>
</Layout>
```

Now that we have all our components fully coded and integrated, we need to take care of the application’s responsiveness. This is where Tailwind can make our life easier, and in a couple of lines, we have fantastic results! ![The mobile view of our app](https://blog.logrocket.com/wp-content/uploads/2023/01/categories-row-chart-product-table.png) The mobile view of our app Let’s go for a mobile-first approach. The Categories tag and overview are rendered in one row; below it, we have the chart, and at the bottom (with some scrolling required) is the Product table. 

This image illustrates the tablet view of our website, offering a balance between the desktop and mobile versions for a convenient browsing experience on a larger screen. ![The tablet view of our app](https://blog.logrocket.com/wp-content/uploads/2023/01/tablet-view.png) Finally, we have the desktop view of our website, with a wide layout and padding added on the sides to center the content horizontally. ![The desktop view of our app](https://blog.logrocket.com/wp-content/uploads/2023/01/desktop-view.png)

## Conclusion

In a previous article, we discussed the architecture behind Astro which makes it possible to build this kind of application. I highly recommend checking out that article in case you are not familiar with [Astro’s implementation of islands architecture](https://blog.logrocket.com/understanding-astro-islands-architecture/). 

Astro is a useful tool that can help us build complex and versatile software by allowing us to combine multiple frameworks. As our software systems and requirements evolve, Astro can assist us in easily and efficiently migrating our applications to new frameworks, saving us time, effort, and resources. 

On top of that, it also helps us optimize our applications by ensuring that we are not using more JavaScript than necessary. This helps keep our application lightweight and performant, which can improve user experience and the overall functionality of the application. This feature of Astro makes it valuable when building efficient and high-performing applications that meet the needs of our users. 

Thanks for reading, and let's keep building stuff together! 👷‍♂️

### References

*   [Astro docs](https://astro.build/)
*   [Multi-framework dashboard - GitHub Repository](https://github.com/EmaSuriano/astro-multi-framework-dashboard)
*   [Multi-framework dashboard - Demo](https://astro-multi-framework-dashboard.netlify.app/)
*   [nanostores - GitHub Repository](https://github.com/nanostores/nanostores)
*   [MUI docs](https://mui.com/)
*   [Vuetify docs](https://next.vuetifyjs.com/en/)
*   [SMUI docs](https://sveltematerialui.com/demo/chips/)

---
##Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

[LogRocket](https://lp.logrocket.com/blg/javascript-signup) is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2019/10/errors-screenshot.png)](https://lp.logrocket.com/blg/javascript-signup)

[LogRocket](https://lp.logrocket.com/blg/javascript-signup) works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — [Start monitoring for free](https://lp.logrocket.com/blg/javascript-signup).