React Integration Guide

Install IssueCapture in React

Add bug reporting to a React app. Works with Create React App, Vite, React Router, and any React 16.8+ setup.

Quick Summary

Time: ~10 minutes (incl. Jira OAuth)
Prerequisites: React 16.8+, Jira Cloud account
Method: Script tag or useEffect inject
Works with: CRA, Vite, React Router

Prerequisites

  • React 16.8 or higher (hooks support)
  • Create React App or Vite (any React bundler with .env support)
  • Jira Cloud account (Software or JSM)
  • IssueCapture account (free — includes 10 issues/month)

Step-by-Step Installation

Follow these steps to get up and running in minutes.

1

Get Your API Key

Sign up for IssueCapture and grab your widget API key

  • Go to issuecapture.com/signup and create a free account
  • Connect your Jira instance via OAuth (about 30 seconds)
  • Open the Widgets page and create a new widget
  • Click the "Keys" tab on your widget and copy the API key (starts with "ic_")
2

Add the API key to your env file

Use the right prefix for your bundler — Vite and CRA differ

  • Vite reads variables prefixed VITE_ via import.meta.env
  • Create React App reads variables prefixed REACT_APP_ via process.env
  • Add .env (or .env.local) to .gitignore — don't commit the key
  • Restart the dev server after adding env vars — neither Vite nor CRA hot-reloads them
# .env  (Vite)
VITE_ISSUECAPTURE_API_KEY=ic_your_api_key_here

# .env  (Create React App)
REACT_APP_ISSUECAPTURE_API_KEY=ic_your_api_key_here
3

Drop the widget into public/index.html

Simplest option: a static script tag, loaded once with the page

  • The widget is an ESM module — type="module" is required
  • Place the snippet just before </body> so it doesn't block first paint
  • CRA only: %REACT_APP_*% tokens in index.html are replaced at build time
  • Vite users: index.html token replacement does not happen — use Step 4 instead
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Your React App</title>
  </head>
  <body>
    <div id="root"></div>

    <!-- IssueCapture Widget (ESM module) -->
    <script type="module">
      import IssueCapture from 'https://issuecapture.com/widget.js';
      IssueCapture.init({
        // CRA inlines %REACT_APP_*% tokens in index.html at build time.
        // Vite users: see Step 4 for a runtime equivalent that reads
        // import.meta.env (Vite does NOT replace these tokens in HTML).
        apiKey: '%REACT_APP_ISSUECAPTURE_API_KEY%',
      });
    </script>
  </body>
</html>
4

Vite-friendly alternative: load from your root component

Inject the widget script from React so import.meta.env can supply the key

  • A dynamically created `<script type="module">` with inline text DOES execute when appended (unlike a parser-inserted module with src; this is the supported pattern)
  • Use `import.meta.env` for Vite, `process.env` for CRA — one will be undefined depending on your bundler
  • Empty dependency array `[]` ensures the widget is injected exactly once per mount
  • The cleanup function is only needed if you really want to tear down the widget — usually you want it for the lifetime of the SPA
// src/App.tsx
import { useEffect } from 'react';

export default function App() {
  useEffect(() => {
    const apiKey =
      // Vite
      import.meta.env?.VITE_ISSUECAPTURE_API_KEY ??
      // CRA
      process.env.REACT_APP_ISSUECAPTURE_API_KEY;

    const s = document.createElement('script');
    s.type = 'module';
    // Inline ESM module — runs on append, lets us use the runtime apiKey
    s.textContent = `
      import IssueCapture from 'https://issuecapture.com/widget.js';
      IssueCapture.init({ apiKey: '${apiKey}' });
    `;
    document.body.appendChild(s);

    return () => {
      // Optional: tear down on unmount (rare — you usually want it global)
      window.IssueCapture?.destroy?.();
      s.remove();
    };
  }, []);

  return <div>{/* your app */}</div>;
}
5

Trigger the widget from your own button

Skip the floating button and open the modal from a React component

  • Optional-chain `window.IssueCapture?.open()` so a missing widget never throws
  • You can use this button alongside the floating one, or set trigger: '#your-id' on init to disable the floating one
  • For accessibility, include aria-label or visible text on the button
// src/components/BugReportButton.tsx
declare global {
  interface Window {
    IssueCapture?: {
      open: () => void;
      close: () => void;
      isOpen: () => boolean;
      updateConfig: (cfg: Record<string, unknown>) => void;
      destroy: () => void;
    };
  }
}

export default function BugReportButton() {
  const open = () => window.IssueCapture?.open();

  return (
    <button
      onClick={open}
      aria-label="Report an issue"
      className="bug-report-btn"
    >
      Report an issue
    </button>
  );
}
6

Pre-fill the logged-in user (optional)

Save your customers some typing — pass name and email through

  • Use updateConfig for runtime changes — init should only fire once per page
  • If the user logs out, you can call updateConfig({ user: { name: '', email: '' } }) to clear it
  • These values pre-populate the modal but the user can still edit them before submitting
// src/App.tsx
import { useEffect } from 'react';
import { useAuth } from './hooks/useAuth'; // your own auth hook

export default function App() {
  const { user } = useAuth();

  // On login (or whenever the user object changes), tell the widget
  // who's reporting. updateConfig is cheap — call it as many times as
  // you need; don't re-call init().
  useEffect(() => {
    if (!user) return;
    window.IssueCapture?.updateConfig({
      user: { name: user.name, email: user.email },
    });
  }, [user]);

  return <div>{/* your app */}</div>;
}
7

Verify it works

Quick sanity check end-to-end

  • Start your dev server: `npm run dev` (Vite) or `npm start` (CRA)
  • Open your app in the browser — you should see the floating Report Issue button
  • Open DevTools console and type `IssueCapture` — you should see the widget object (this distinguishes "script didn't load" from "ad blocker is hiding the button")
  • Click the button, submit a test issue, and confirm it lands in Jira

Troubleshooting

Common integration issues and how to solve them.

TypeScript: Property 'IssueCapture' does not exist on type 'Window'

Add a global declaration to your project (src/types/issuecapture.d.ts)

  • Create `src/types/issuecapture.d.ts` declaring `IssueCapture` on the global Window interface
  • Make sure that path is covered by `tsconfig.json` `include`
  • For the full method surface (init, on, updateConfig, destroy, etc.) see the IssueCaptureAPI types in the docs

Environment variables aren't reaching the browser

  • CRA: variables MUST be prefixed `REACT_APP_`
  • Vite: variables MUST be prefixed `VITE_` and accessed via `import.meta.env`
  • Restart the dev server after editing .env
  • NEXT_PUBLIC_ is a Next.js-only prefix — it won't work in CRA or Vite

Widget loads twice when navigating with React Router

  • Initialise the widget once at the root (index.html or App.tsx with empty deps)
  • Don't put the IssueCapture script tag inside a per-route component
  • If you do need per-route config, use IssueCapture.updateConfig() — never re-call init()

Need different widget configs per page

  • Call `IssueCapture.updateConfig({ apiKey: '...' })` on route change
  • For full teardown (rare) call `IssueCapture.destroy()` then `init` again with the new config
  • For light tweaks (user, default values), updateConfig is enough

"Domain not allowed" error (often shows up as CORS in the console)

  • In the IssueCapture dashboard, open your widget and click the Domains tab
  • Add `localhost:3000` (CRA) or `localhost:5173` (Vite) for local dev
  • Add your production domain — both www and non-www if you serve both
  • Subdomains need to be listed individually

Ready to get started?

Free plan includes 10 issues/month. No card needed — connect Jira and you're done.