React 18 and React 19
Install @belvador/react for browser React apps (Vite, SPA), with separate wiring for React 18 and React 19.
On this page
Use @belvador/react for React apps that render in the browser — Vite, Create React App or any other single-page setup. It includes everything in @belvador/browser, plus an error boundary and React 19 root hooks. Lovable and TanStack Start apps use TanStack Start instead.
Before you start#
A project with platform React (Vite / SPA) and its DSN.
React 18 or React 19. Check which one you have:
npm ls react.
1. Install#
npm install @belvador/react@0.6.12. Initialize#
Create src/instrument.ts. It starts Belvador:
import { init } from "@belvador/react";init({ dsn: "https://<public-key>@ingest.belvador.dev/<project-id>", release: import.meta.env.VITE_APP_VERSION, // your build or commit id environment: import.meta.env.MODE,});3. Import it first — React 18#
Make ./instrument the first import of src/main.tsx. Imports run before the rest of a file, so an init call written below import App never runs when one of App's modules fails while loading — a missing environment variable, for example. The page stays blank and nothing is reported. This version works with React 18 and 19. ErrorBoundary reports render errors (with the component stack) and shows the fallback instead of a blank page.
import "./instrument"; // first line: starts Belvador before App's modules loadimport { createRoot } from "react-dom/client";import { ErrorBoundary } from "@belvador/react";import App from "./App";const root = document.getElementById("root");if (!root) throw new Error("Root element missing");createRoot(root).render( <ErrorBoundary fallback={<p>Something went wrong.</p>}> <App /> </ErrorBoundary>,);3. Import it first — React 19#
React 19 handles many errors itself and does not let them reach window.onerror. Pass reactErrorHandler() to the three root options so Belvador sees them. These options do not exist in React 18 — use the version above there.
import "./instrument"; // first line: starts Belvador before App's modules loadimport { createRoot } from "react-dom/client";import { ErrorBoundary, reactErrorHandler } from "@belvador/react";import App from "./App";const root = document.getElementById("root");if (!root) throw new Error("Root element missing");createRoot(root, { onUncaughtError: reactErrorHandler(), onCaughtError: reactErrorHandler(), onRecoverableError: reactErrorHandler(),}).render( <ErrorBoundary fallback={<p>Something went wrong.</p>}> <App /> </ErrorBoundary>,);| Root option | Reports |
|---|---|
onUncaughtError | Errors no error boundary caught |
onCaughtError | Errors an error boundary caught and rendered a fallback for |
onRecoverableError | Errors React recovered from on its own, for example hydration mismatches |
reactErrorHandler takes an optional callback that runs after reporting: reactErrorHandler((error, errorInfo, eventId) => { … }).
4. Allow the ingest address in your CSP#
connect-src 'self' https://ingest.belvador.dev;Merge this into your existing connect-src; do not replace the policy.
Error boundaries for parts of the page#
Wrap risky parts of the UI so one failing widget does not take down the page:
import { ErrorBoundary, withErrorBoundary } from "@belvador/react";export function Dashboard() { return ( <ErrorBoundary fallback={({ resetError }) => ( <div role="alert"> <p>This chart failed to load.</p> <button onClick={resetError}>Try again</button> </div> )} > <RevenueChart /> </ErrorBoundary> );}// Or wrap a component once:export const SafeRevenueChart = withErrorBoundary(RevenueChart, { fallback: <p>This chart failed to load.</p>,});ErrorBoundary reports each render error once and shows fallback. The user-feedback dialog some SDKs offer is not available: it would load a script from the network.
Report errors you handle#
Everything from @belvador/browser is exported here too:
import { captureException, setUser } from "@belvador/react";setUser({ id: user.id });try { await importCsv(file);} catch (error) { captureException(error); setStatus("failed");}Check that it works#
Add a temporary button, click it, and open the project in Belvador:
function Crash(): never { throw new Error("Belvador React test");}export function TestButton() { const [crash, setCrash] = useState(false); return crash ? <Crash /> : <button onClick={() => setCrash(true)}>Send test error</button>;}The issue shows the component stack under Contexts. Remove the button afterwards.
Limits to know#
Event handlers (
onClick, …) and async code are not covered by error boundaries — React's rule. Errors there that nobody catches still reach Belvador through the global handlers; errors you catch needcaptureException.Server-side rendering frameworks other than TanStack Start are not covered by this package's setup.
Posts on this topic
All posts- Monitoring vibe-coded apps: what breaks after you publishWhat breaks in a vibe-coded app after you publish, why uptime checks still see 200 OK, and the three checks that catch it. Tested on five real failures.
- Production source maps with Vite: what each setting really shipsVite sourcemap false, true and hidden compared on one build: what ships, what a stack frame shows, and how to get readable stacks without public maps.
- React error boundaries and async errors: what our lab caughtWhich errors a React 19 error boundary catches, which reach onCaughtError, onUncaughtError or window, and why standalone startTransition differed.
Spotted something wrong or missing? Tell us: hello@belvador.dev