Developer installation guide

How to embed Google reviews in React (Vite or CRA)

Load the saved widget when the review component appears, then clear its generated display when that component leaves the page.

Updated October 2026

Quick answer

  1. Simplest: add <div data-verifiedwidget-id="YOUR_WIDGET_ID" /> in your component, and load https://verifiedwidget.com/widget.js once in index.html (without data-widget-id).
  2. Or use the GoogleReviews component below, which loads the dashboard script itself and removes it on unmount.
  3. Deploy and check the real URL.

Both routes work in Vite and Create React App (CRA). The React team deprecated CRA in February 2025, but existing CRA apps still run this code. Pick one, not both, or you will see two widgets.

Prepare a real widget first

In VerifiedWidget, connect a business with its Google Maps link. Confirm that its imported reviews are visible, make a widget, choose a layout, and save it. Open the Embed dialog and copy the script. The dashboard gives you this format; the ID shown here is an example only, so replace it with the ID in your own code:

<script src="https://verifiedwidget.com/widget.js" data-widget-id="c123456789012345678901234" crossorigin="anonymous" async></script>

The script address and attributes are public install code. They do not expose a private Google API key. In a static HTML file, you can paste that tag exactly. In a React page, the review area may be mounted after the browser first loads, and navigation may swap components without reloading the document. A component can load the script at the right time and remove the generated display on cleanup.

Simpler option: a placeholder

Load the script once in index.html (in the project root for Vite, or public/index.html in Create React App), without data-widget-id. It then fills any element marked with data-verifiedwidget-id, including ones React adds later when a route changes. No effect or cleanup code is needed, because the loader watches for new placeholders and React removes the element (and its reviews) when the component unmounts.

<!-- index.html: load the loader once, without data-widget-id -->
<script src="https://verifiedwidget.com/widget.js" crossorigin="anonymous" async></script>
export function GoogleReviews() {
  return (
    <section aria-labelledby="reviews-heading">
      <h2 id="reviews-heading">Customer reviews</h2>
      <div data-verifiedwidget-id="c123456789012345678901234" style={{ minHeight: 320 }} />
    </section>
  );
}

Keep the placeholder element empty; do not render children into it. The ID shown is an example, so use your own widget ID. Do not also add the dashboard script (the one with data-widget-id) for the same widget.

Or load the script from a component

Create GoogleReviews.tsx in your Vite or Create React App project. The code below uses only React and browser APIs. Change WIDGET_ID to the value from your saved dashboard snippet. Keep the address, crossOrigin, and async settings from that snippet. If you use JavaScript instead of TypeScript, remove <HTMLDivElement> from the ref.

import { useEffect, useRef } from "react";

const WIDGET_ID = "c123456789012345678901234"; // Replace with your widget ID.

export function GoogleReviews() {
  const hostRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const host = hostRef.current;
    if (!host) return;

    const script = document.createElement("script");
    script.src = "https://verifiedwidget.com/widget.js";
    script.dataset.widgetId = WIDGET_ID;
    script.crossOrigin = "anonymous";
    script.async = true;
    host.appendChild(script);

    return () => {
      script.remove();
      host.replaceChildren();
    };
  }, []);

  return <div ref={hostRef} style={{ minHeight: 320 }} aria-label="Customer reviews" />;
}

useEffect runs in the browser after React has put the empty host element on the page. The loader reads data-widget-id from the script as it runs and inserts its display beside that script inside the host. The host ref keeps React from trying to manage the review cards that the external loader creates. The cleanup removes both the script and its generated display when the component unmounts.

The minimum height is a starting point for a grid or carousel. Adjust it for your actual layout and phone width. A floating badge needs none, because it sits in a screen corner; a long list needs more.

Render it on the intended route

Import the component on the page where reviews belong. Put a normal heading outside the widget so the section has context even before the script loads. For example:

import { GoogleReviews } from "./GoogleReviews";

export function HomePage() {
  return (
    <main>
      <h1>Welcome to our business</h1>
      <section aria-labelledby="reviews-heading">
        <h2 id="reviews-heading">Customer reviews</h2>
        <GoogleReviews />
      </section>
    </main>
  );
}

The bundler does not change the embed code. Publish the app and check its actual URL, not just the development preview.

Place the component only on routes that should show reviews. A router may keep a parent layout mounted while child pages change. If you put the component in that persistent layout, it will stay visible on every child route. If you put it in the specific review page, it unmounts when you leave and remounts when you return. That lifecycle is what the cleanup above is designed for.

Handle single-page navigation and development reloads

React routers normally change the visible page without a full browser refresh. Merely adding the original dashboard script (with data-widget-id) to index.html can load it before the review route appears and place the result in the wrong spot. Loading it from the route component means the script arrives with the section. When visitors leave, the cleanup clears the old cards. When they navigate back, the new component loads its own script and display.

React's development Strict Mode can run an effect setup, its cleanup, and setup again to check that cleanup works. You may see extra script activity in development. The code clears the first host before the next setup; check the production build before judging duplicate behavior. If a route stays mounted while only its contents change, key the review component to the route or ensure the host is recreated at the intended position. Do not append several copies of the script to the same host without removing the previous one.

Do not mix the two methods for the same widget: a placeholder plus a dashboard script tag produces a duplicate display.

Badge and review pop-up layouts are different. They float over the page, so the loader moves them to the end of the page body. React cleanup does not remove them, and a return visit to the route adds a second one. For a floating layout, render the component or placeholder once in a layout that stays mounted on every route.

Security and loading checks

If your deployment uses a Content Security Policy, allow https://verifiedwidget.com in both script-src and connect-src. The first covers widget.js; the second covers its review-data request. Review images may use other hosts, so inspect the browser console before changing img-src. Do not place a secret Google key in a Vite VITE_ variable or a Create React App REACT_APP_ variable: those values are bundled for visitors. The widget embed needs no such key in your app.

Test desktop and phone widths after publishing. Confirm review names, dates, links, and any media match the connected business. If you have enabled a domain allowlist, add both your live host and the preview host you use for testing. A staging subdomain is a different host from your main domain. If you use a consent manager or browser extension that blocks outside scripts, test the intended visitor path and confirm the widget eventually appears.

Troubleshoot a missing or duplicate widget

If the section is blank, inspect the page after deployment. Confirm the ID is your saved widget ID and that the component actually mounted on that route. In the browser network panel, look for widget.js and the /api/embed/ request. A blocked script points to the browser or script-src; a blocked data call can point to connect-src or domain restrictions. The loader reports a 403 for an unauthorized domain, a 404 for an unknown widget, and a 429 when requests arrive too quickly.

If the calls succeed but there are no cards, check that the widget has imported reviews and that its filters have not hidden every review. If two copies appear, search the project for another embed in index.html, a persistent layout, or a second component. If the display appears on the wrong route, check where the component is rendered and whether a parent layout remains mounted during navigation. If the review area is clipped, inspect the host section's width and overflow.

If your app uses the Next.js App Router, follow the Next.js guide instead. For Google star results, see our star ratings guide.

Frequently asked questions

Does this work with Vite and Create React App?

Yes. The example needs React, a browser, and your saved widget ID; the bundler does not change the embed format.

Why load the script in useEffect?

The effect runs after the host element exists in the browser, so the loader can place reviews inside the right component on that route.

What happens when a visitor changes routes?

The component cleanup removes the previous script and display. Returning to the route mounts a fresh component and loads the widget again.

Why do I see two widgets during development?

Check for an additional script in index.html or a parent layout. React Strict Mode also repeats effect setup in development, so verify the production build.

Can I put a Google API key in a React environment variable?

Do not put a private key in client-side React configuration. The VerifiedWidget embed only needs the public widget ID and script URL.

Show real reviews in your React app

Create a free widget, copy its ID, and add the component to the route where visitors should see it.

Related reading