# Use it with React and Vue

> Put the mindset-agent element in a React 19 or Vue 3 app, resume conversations, and load the script only when a user opens the chat.

After this page the drop-in element runs in your React 19 or Vue 3 app, resumes each user's conversation, and downloads only when someone opens it.

There is no wrapper package for either framework. Both render custom elements directly, so you use `<mindset-agent>` as a tag and call its methods through a ref. The examples use the `/api/session` endpoint from [Put an agent on your page](https://docs4.mindset.ai/docs/sdk/put-an-agent-on-your-page).

## Load the script once

Every approach below needs `mindset-agent.js` on the page exactly once. Either add it to your `index.html`:

```html
<script src="https://eu.mindset.ai/sdk/mindset-agent.js"></script>
```

or load it from code when the chat first opens, as in [load it only when it's needed](#load-it-only-when-it-is-needed). Don't do both, and don't load it from a component that can mount twice. A second copy logs a tag collision error. Never add `mindset-agent-charts.js` yourself; the element fetches it when it draws a chart.

## React 19

React 19 passes props to custom elements and renders them like any tag. This component configures the element once, resumes a stored conversation, and reports the conversation ID and errors to your code.

```tsx
import { useEffect, useRef, type HTMLAttributes, type Ref } from "react";

type MindsetAgentElement = HTMLElement & {
  configure(config: {
    getSession: () => Promise<unknown>;
    conversationId?: string;
    conversationList?: boolean;
  }): void;
};

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "mindset-agent": HTMLAttributes<HTMLElement> & {
        agent?: string;
        ref?: Ref<HTMLElement>;
      };
    }
  }
}

async function getSession() {
  const r = await fetch("/api/session");
  if (!r.ok) throw new Error("Could not start a session");
  return r.json();
}

type Props = {
  conversationId?: string;
  onConversation: (conversationId: string) => void;
};

export function BillingAgent({ conversationId, onConversation }: Props) {
  const ref = useRef<HTMLElement>(null);
  const configured = useRef(false);
  const onConversationRef = useRef(onConversation);
  onConversationRef.current = onConversation;

  useEffect(() => {
    const el = ref.current as MindsetAgentElement | null;
    if (!el) return;

    const handleConversation = (e: Event) => {
      const detail = (e as CustomEvent<{ conversationId: string }>).detail;
      onConversationRef.current(detail.conversationId);
    };
    const handleError = (e: Event) => {
      const detail = (e as CustomEvent<{ code: string; message: string }>).detail;
      console.error("Mindset error:", detail.code, detail.message);
    };
    el.addEventListener("mindset:conversation", handleConversation);
    el.addEventListener("mindset:error", handleError);

    // StrictMode runs this effect twice in development. Configure once.
    if (!configured.current) {
      configured.current = true;
      el.configure({
        getSession,
        conversationList: true,
        ...(conversationId ? { conversationId } : {}),
      });
    }

    return () => {
      el.removeEventListener("mindset:conversation", handleConversation);
      el.removeEventListener("mindset:error", handleError);
    };
  }, []);

  return <mindset-agent ref={ref} agent="billing-help" />;
}
```

Things that catch people out in React:

- **StrictMode creates two sessions without the guard.** In development, React runs each effect, cleans it up and runs it again. Each `configure()` call asks your backend for a session, so an unguarded effect creates two. The `configured` ref keeps it to one, because React keeps refs across that second run.
- **Pass `conversationId` to `configure()`, not as a JSX attribute.** The element writes its own `conversation-id` attribute after the first turn. Don't render that attribute from JSX, where a re-render could write a different value back and stop the conversation.
- **Keep `agent` fixed.** Changing the `agent` prop after `configure()` stops the conversation and raises `stale-configuration`. To switch agents, render a new component with a `key` that changes with the agent, so React mounts a fresh element.
- **Listen with `addEventListener`.** The element's events have a colon in their names (`mindset:error`). Adding the listener in an effect, as above, works in every React version.

Store the ID your `onConversation` callback receives against the signed-in user, and pass it back as `conversationId` on their next visit.

## Vue 3

Vue's template compiler warns about any tag it can't resolve, unless you tell it the tag is a custom element. With Vite, set that in `vite.config.ts`:

```typescript
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag === "mindset-agent",
        },
      },
    }),
  ],
});
```

Then use the element in a component, and configure it once it's mounted:

```vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";

const props = defineProps<{ conversationId?: string }>();
const emit = defineEmits<{ conversation: [conversationId: string] }>();

type MindsetAgentElement = HTMLElement & {
  configure(config: {
    getSession: () => Promise<unknown>;
    conversationId?: string;
    conversationList?: boolean;
  }): void;
};

const agentEl = ref<MindsetAgentElement | null>(null);

function handleConversation(e: Event) {
  const detail = (e as CustomEvent<{ conversationId: string }>).detail;
  emit("conversation", detail.conversationId);
}

function handleError(e: Event) {
  const detail = (e as CustomEvent<{ code: string; message: string }>).detail;
  console.error("Mindset error:", detail.code, detail.message);
}

onMounted(() => {
  const el = agentEl.value;
  if (!el) return;
  el.addEventListener("mindset:conversation", handleConversation);
  el.addEventListener("mindset:error", handleError);
  el.configure({
    getSession: async () => {
      const r = await fetch("/api/session");
      if (!r.ok) throw new Error("Could not start a session");
      return r.json();
    },
    conversationList: true,
    ...(props.conversationId ? { conversationId: props.conversationId } : {}),
  });
});

onBeforeUnmount(() => {
  agentEl.value?.removeEventListener("mindset:conversation", handleConversation);
  agentEl.value?.removeEventListener("mindset:error", handleError);
});
</script>

<template>
  <mindset-agent ref="agentEl" agent="billing-help" />
</template>
```

The same rules apply as in React: configure once, pass `conversationId` to `configure()` rather than binding the attribute, and keep `agent` fixed for the life of the element.

If you don't use Vite, set `app.config.compilerOptions.isCustomElement` instead. It only works when Vue compiles templates in the browser. With a build step, set the option in your build tool's Vue plugin.

## Load it only when it is needed

The drop-in script is about 1.5 MB. If most visitors never open the chat, load it on the first open. This plain JavaScript works in any page or framework:

```javascript
const SDK_URL = "https://eu.mindset.ai/sdk/mindset-agent.js";
let sdkReady;

function loadMindsetSdk() {
  sdkReady ??= new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = SDK_URL;
    script.onload = () => resolve(customElements.whenDefined("mindset-agent"));
    script.onerror = () => {
      sdkReady = undefined;
      reject(new Error("The Mindset SDK could not be loaded"));
    };
    document.head.appendChild(script);
  });
  return sdkReady;
}

async function openChat() {
  await loadMindsetSdk();
  const agent = document.createElement("mindset-agent");
  agent.setAttribute("agent", "billing-help");
  document.querySelector("#chat").appendChild(agent);
  agent.configure({
    getSession: async () => {
      const r = await fetch("/api/session");
      if (!r.ok) throw new Error("Could not start a session");
      return r.json();
    },
  });
}

document.querySelector("#open-chat").addEventListener("click", openChat, { once: true });
```

`#chat` and `#open-chat` stand for your own container and button. Keep the script on your Mindset host: the element works out where to fetch charts and fonts from the address the script was loaded from. If your page uses a nonce-based content security policy, set `script.nonce` before appending it. See [Content security policy](https://docs4.mindset.ai/docs/sdk/content-security-policy).

In React or Vue, call `loadMindsetSdk()` before rendering the component that holds the tag, for example in the click handler that opens the panel.

## The UI-less client in a bundled app

The UI-less client is an ES module on your Mindset host. Import it with a dynamic `import()` at runtime, and tell your bundler not to resolve the address:

```javascript
const UILESS_URL = "https://eu.mindset.ai/sdk/mindset-agent-uiless.js";

// Vite: /* @vite-ignore */   webpack: /* webpackIgnore: true */
const { createAgentConversation } = await import(/* @vite-ignore */ UILESS_URL);
```

In React, create the conversation once (for example in a ref guarded the same way as `configure()` above), because each `createAgentConversation` asks your backend for a session. [The UI-less client](https://docs4.mindset.ai/docs/sdk/the-ui-less-client) covers the rest.

## Related

- [Layout and theming](https://docs4.mindset.ai/docs/sdk/layout-and-theming) to size and color the element.
- [The mindset-agent element](https://docs4.mindset.ai/docs/sdk/the-mindset-agent-element) for every method and event.
