React, Vue, Next.js, Nuxt & Angular Integration Guide

Single-page app installation, including router-aware mounting.

Medium10 minFreeSPA routing

QuackChat in Single-Page Applications

This guide covers integrating the QuackChat widget into React, Vue, Next.js, Nuxt, and Angular applications.

Key Considerations for SPAs

Single-page applications have unique requirements:

  1. Lifecycle management: Widget must be initialized after mount and cleaned up on unmount
  2. Route changes: Widget persists across route changes (usually desired)
  3. SSR compatibility: Script must only load on the client side
  4. TypeScript support: Type definitions for the global QuackChatWidget object

React

Basic Integration

tsx
// components/QuackChatWidget.tsx
import { useEffect } from 'react';

interface QuackChatWidgetProps {
  botId: string;
  position?: 'bottom-right' | 'bottom-left';
  primaryColor?: string;
}

export function QuackChatWidget({
  botId,
  position = 'bottom-right',
  primaryColor = '#2563eb',
}: QuackChatWidgetProps) {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://www.quackchat.app/widget/quackchat-widget.js';
    script.async = true;
    script.dataset.botId = botId;
    script.dataset.position = position;
    script.dataset.primaryColor = primaryColor;

    document.body.appendChild(script);

    return () => {
      if ((window as any).QuackChatWidget) {
        (window as any).QuackChatWidget.destroy();
      }
      document.body.removeChild(script);
    };
  }, [botId, position, primaryColor]);

  return null;
}

Usage

tsx
// App.tsx
import { QuackChatWidget } from './components/QuackChatWidget';

function App() {
  return (
    <>
      <YourAppContent />
      <QuackChatWidget botId="YOUR_BOT_ID" primaryColor="#4f46e5" />
    </>
  );
}

Vue

Basic Integration (Vue 3 Composition API)

vue
<!-- components/QuackChatWidget.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';

const props = defineProps<{
  botId: string;
  position?: string;
  primaryColor?: string;
}>();

let script: HTMLScriptElement | null = null;

onMounted(() => {
  script = document.createElement('script');
  script.src = 'https://www.quackchat.app/widget/quackchat-widget.js';
  script.async = true;
  script.dataset.botId = props.botId;
  if (props.position) script.dataset.position = props.position;
  if (props.primaryColor) script.dataset.primaryColor = props.primaryColor;

  document.body.appendChild(script);
});

onUnmounted(() => {
  if ((window as any).QuackChatWidget) {
    (window as any).QuackChatWidget.destroy();
  }
  if (script) script.remove();
});
</script>

<template></template>

Next.js

App Router (Next.js 13+)

tsx
// components/QuackChatWidget.tsx
'use client';

import { useEffect } from 'react';

export function QuackChatWidget({ botId }: { botId: string }) {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://www.quackchat.app/widget/quackchat-widget.js';
    script.async = true;
    script.dataset.botId = botId;

    document.body.appendChild(script);

    return () => {
      if ((window as any).QuackChatWidget) {
        (window as any).QuackChatWidget.destroy();
      }
      document.body.removeChild(script);
    };
  }, [botId]);

  return null;
}

Usage in Layout

tsx
// app/layout.tsx
import { QuackChatWidget } from '@/components/QuackChatWidget';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <QuackChatWidget botId="YOUR_BOT_ID" />
      </body>
    </html>
  );
}

Angular

Service Approach

typescript
// services/quackchat.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class QuackChatService {
  private script: HTMLScriptElement | null = null;

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  init(config: { botId: string }) {
    if (!isPlatformBrowser(this.platformId)) return;

    this.script = document.createElement('script');
    this.script.src = 'https://www.quackchat.app/widget/quackchat-widget.js';
    this.script.async = true;
    this.script.dataset['botId'] = config.botId;

    document.body.appendChild(this.script);
  }

  destroy() {
    if ((window as any).QuackChatWidget) {
      (window as any).QuackChatWidget.destroy();
    }
    if (this.script) {
      document.body.removeChild(this.script);
      this.script = null;
    }
  }
}

Troubleshooting

Widget loads multiple times

Cause: Component re-renders without cleanup.

Solution: Ensure cleanup function removes the script and calls destroy().

"QuackChatWidget is not defined"

Cause: Script hasn't loaded yet.

Solution: Check if widget exists before calling methods:

typescript
if ((window as any).QuackChatWidget) {
  (window as any).QuackChatWidget.open();
}

SSR errors ("window is not defined")

Cause: Trying to access window during server-side rendering.

Solution:

  • Use useEffect (React) or onMounted (Vue)
  • Use 'use client' directive (Next.js App Router)
  • Check isPlatformBrowser (Angular)

Memory leaks

Cause: Widget not destroyed on component unmount.

Solution: Always call destroy() in cleanup.

Stop letting warm leads go cold

Start converting while they're still on your site

Build it and test it free, no card. When you put it live on your site that starts 30 days free, cancel any time, and you're reminded before you're charged.