Install IssueCapture in React
Add bug reporting to a React app with Create React App or Vite. Includes code-splitting, per-route context-based config via React Router, and CRA-vs-Vite env inlining gotchas.
Quick Summary
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.
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_")
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_hereDrop 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>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>;
}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>
);
}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>;
}Code-split the widget to keep your bundle lean (advanced)
Defer widget injection until first use — saves ~31KB from initial bundle when using React.lazy()
- Lazy-loading defers the 31KB widget code until your app mounts or the user first interacts with the feedback button
- Wrap the lazy component in Suspense (though the fallback can be null since the widget loads asynchronously anyway)
- This is optional — if your bundle is small, loading the widget at root is simpler and more reliable
- Avoid lazy-loading if you want the button visible before user interaction — trade-offs between initial paint time and First Contentful Paint (FCP)
// src/App.tsx
import { lazy, Suspense, useEffect, useState } from 'react';
const IssueCaptureLazy = lazy(async () => {
const apiKey =
import.meta.env?.VITE_ISSUECAPTURE_API_KEY ??
process.env.REACT_APP_ISSUECAPTURE_API_KEY;
if (!apiKey) return { default: () => null };
// Load the widget script once on first render
const s = document.createElement('script');
s.type = 'module';
s.textContent = `
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({ apiKey: '${apiKey}' });
`;
document.body.appendChild(s);
return { default: () => null };
});
export default function App() {
const [showWidget, setShowWidget] = useState(false);
useEffect(() => {
// Load widget on first interaction (e.g. when user hovers over button)
setShowWidget(true);
}, []);
return (
<div>
{showWidget && <Suspense fallback={null}><IssueCaptureLazy /></Suspense>}
{/* your app */}
</div>
);
}React Context + React Router: per-route widget config (optional)
Vary the widget config (API key, default values, labels) on each route
- Define a mapping of routes to widget configs (different API keys, default form values, labels)
- Use React Router's useLocation hook to detect route changes
- Call updateConfig on each route change — this is cheap and doesn't re-initialize the widget
- Optional: expose the context so child components can read the current config
// src/IssueCaptureContext.tsx
import { createContext, useContext, useEffect, ReactNode } from 'react';
import { useLocation } from 'react-router-dom';
interface WidgetConfig {
apiKey: string;
defaultValues?: Record<string, unknown>;
labels?: string[];
}
const IssueCaptureContext = createContext<WidgetConfig | null>(null);
export function IssueCaptureProvider({ children }: { children: ReactNode }) {
const location = useLocation();
const routeConfigs: Record<string, WidgetConfig> = {
'/support': {
apiKey: 'ic_support_widget',
defaultValues: { component: 'Support' },
},
'/billing': {
apiKey: 'ic_billing_widget',
defaultValues: { component: 'Billing', priority: 'High' },
},
};
const currentConfig = routeConfigs[location.pathname] || {
apiKey: 'ic_default',
};
useEffect(() => {
// Update widget config whenever route changes
window.IssueCapture?.updateConfig(currentConfig);
}, [location.pathname, currentConfig]);
return (
<IssueCaptureContext.Provider value={currentConfig}>
{children}
</IssueCaptureContext.Provider>
);
}
export const useIssueCapture = () => useContext(IssueCaptureContext);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
CRA: env var changes not applying — still seeing old value in browser
Create React App caches .env values at build time in `node_modules/.cache`. Vite does NOT cache in the same way.
- Delete `node_modules/.cache` and `.env.local.bak` (CRA creates these)
- Restart `npm start` after editing .env
- CRA does NOT hot-reload env vars — if you change .env, the dev server will NOT pick it up until restart
- Vite DOES watch .env for changes — no restart needed for Vite projects
- This is one reason Vite is increasingly preferred over Create React App for new React projects
CRA (process.env) vs Vite (import.meta.env) — which should I use?
They access env vars differently, and CRA/Vite bundlers replace them at build time, not runtime.
- CRA: Variables are injected into the HTML bundle at build time; use `%REACT_APP_*%` token syntax in index.html OR process.env in JS
- Vite: `import.meta.env.*` is replaced at build time; use `import.meta.env.VITE_*` in JS (NOT in index.html)
- The step-2 code above shows both patterns: Step 3 (CRA direct) and Step 4 (Vite runtime load)
- Choosing Vite simplifies multi-bundler projects because you use one consistent pattern (import.meta.env)
Ready to get started?
Free plan includes 10 issues/month. No card needed — connect Jira and you're done.