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